From 150bfce43b869339bf9432ab55280313f21d6fcc Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 27 Aug 2024 12:47:00 +0530 Subject: [PATCH 01/15] WIP: active activity listener to setup copy item exclusion patterns --- packages/copycat_base | 2 +- packages/copycat_pro | 2 +- plugins/focus_window/lib/activity_info.dart | 119 +++++++ .../lib/focus_window_method_channel.dart | 55 ++-- .../lib/focus_window_platform_interface.dart | 51 ++- plugins/focus_window/lib/macos.dart | 100 ++++++ plugins/focus_window/lib/not_supported.dart | 58 ++++ .../platform_activity_observer_interface.dart | 18 ++ plugins/focus_window/lib/utils.dart | 20 ++ plugins/focus_window/lib/windows.dart | 158 +++++++++ .../macos/Classes/EventCallback.swift | 29 ++ .../macos/Classes/EventManager.swift | 43 +++ .../macos/Classes/FocusWindowPlugin.swift | 303 +++++++++++++++++- plugins/focus_window/pubspec.yaml | 3 + .../focus_window/window_utils/GetActivity.ps1 | 33 ++ plugins/focus_window/window_utils/GetIcon.ps1 | 15 + plugins/focus_window/window_utils/GetUrl.ps1 | 59 ++++ 17 files changed, 1033 insertions(+), 35 deletions(-) create mode 100644 plugins/focus_window/lib/activity_info.dart create mode 100644 plugins/focus_window/lib/macos.dart create mode 100644 plugins/focus_window/lib/not_supported.dart create mode 100644 plugins/focus_window/lib/platform_activity_observer_interface.dart create mode 100644 plugins/focus_window/lib/utils.dart create mode 100644 plugins/focus_window/lib/windows.dart create mode 100644 plugins/focus_window/macos/Classes/EventCallback.swift create mode 100644 plugins/focus_window/macos/Classes/EventManager.swift create mode 100644 plugins/focus_window/window_utils/GetActivity.ps1 create mode 100644 plugins/focus_window/window_utils/GetIcon.ps1 create mode 100644 plugins/focus_window/window_utils/GetUrl.ps1 diff --git a/packages/copycat_base b/packages/copycat_base index 2c60e382..c745857c 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 2c60e382d14fa41f828dac8b1f5b4f39cf15e262 +Subproject commit c745857cc1d1e6d54acfb9c34e681101680a8527 diff --git a/packages/copycat_pro b/packages/copycat_pro index 7d2d93f2..25991d99 160000 --- a/packages/copycat_pro +++ b/packages/copycat_pro @@ -1 +1 @@ -Subproject commit 7d2d93f2f70931cb20888c13b69efd452dd840f5 +Subproject commit 25991d99375bf40af7f27d3e4544ce3c8d8b96fa diff --git a/plugins/focus_window/lib/activity_info.dart b/plugins/focus_window/lib/activity_info.dart new file mode 100644 index 00000000..cb9e2f6a --- /dev/null +++ b/plugins/focus_window/lib/activity_info.dart @@ -0,0 +1,119 @@ +// models +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; + +class ActivityInfo extends Equatable { + // pid: 1175, Done mac(✅) windows (✅) linux (🕒) + // app: 'Code', Done mac(✅) windows (✅) linux (🕒) + // appFileName: 'Visual Studio Code.app', Done mac(✅) windows (✅) linux (🕒) + // appFilePath: '/Applications/Visual Studio Code.app', Done mac(✅) windows (✅) linux (🕒) + // title: '', Done mac(✅) windows (✅) linux (🕒) + // url: '' Done mac(✅) windows (🩹) linux (🕒) + // icon: '' Done mac(✅) windows (✅) linux (🕒) + // identifier: '' Done mac(✅) windows (❌) linux (🕒) + // document: '' Done mac(🕒) windows (🕒) linux (🕒) + + final int pid; + final String app; + final String appFileName; + final String appFilePath; + final String title; + final String url; + final Uint8List? icon; + final String identifier; + final String document; + + const ActivityInfo({ + required this.pid, + required this.app, + required this.appFileName, + required this.appFilePath, + required this.title, + required this.url, + this.icon, + required this.identifier, + required this.document, + }); + + ActivityInfo copyWith({ + int? pid, + String? app, + String? appFileName, + String? appFilePath, + String? title, + String? url, + Uint8List? icon, + String? identifier, + String? document, + }) { + return ActivityInfo( + pid: pid ?? this.pid, + app: app ?? this.app, + appFileName: appFileName ?? this.appFileName, + appFilePath: appFilePath ?? this.appFilePath, + title: title ?? this.title, + url: url ?? this.url, + icon: icon ?? this.icon, + identifier: identifier ?? this.identifier, + document: document ?? this.document, + ); + } + + Map toMap() { + return { + 'pid': pid, + 'app': app, + 'appFileName': appFileName, + 'appFilePath': appFilePath, + 'title': title, + 'url': url, + 'icon': icon, + 'identifier': identifier, + 'document': document, + }; + } + + factory ActivityInfo.fromMap(Map map) { + return ActivityInfo( + pid: map['pid'] ?? int.parse(map['Id']) ?? -1, + app: map['app'] ?? + map['Description'] ?? + map['Product'] ?? + map['Name'] ?? + '', + appFileName: map['appFileName'] ?? map['Name'] ?? '', + appFilePath: map['appFilePath'] ?? map['Path'] ?? '', + title: map['title'] ?? map['MainWindowTitle'] ?? '', + url: map['url'] ?? '', + icon: map['icon'] != null ? map['icon'] as Uint8List : null, + identifier: map['identifier'] ?? '', + document: map['document'] ?? '', + ); + } + + String toJson() => json.encode(toMap()); + + factory ActivityInfo.fromJson(String source) => + ActivityInfo.fromMap(json.decode(source)); + + @override + String toString() { + return 'ActivityInfo(pid: $pid, app: $app, appFileName: $appFileName, appFilePath: $appFilePath, title: $title, url: $url, icon: $icon, identifier: $identifier, document: $document)'; + } + + @override + List get props { + return [ + pid, + app, + appFileName, + appFilePath, + title, + url, + identifier, + document, + ]; + } +} diff --git a/plugins/focus_window/lib/focus_window_method_channel.dart b/plugins/focus_window/lib/focus_window_method_channel.dart index 8966a251..1c863f7e 100644 --- a/plugins/focus_window/lib/focus_window_method_channel.dart +++ b/plugins/focus_window/lib/focus_window_method_channel.dart @@ -1,8 +1,9 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:focus_window/windows_paste_simulator.dart'; +import 'package:focus_window/macos.dart'; +import 'package:focus_window/not_supported.dart'; +import 'package:focus_window/platform_activity_observer_interface.dart'; +import 'package:focus_window/windows.dart'; import 'package:universal_io/io.dart'; -import "package:win32/win32.dart" show GetForegroundWindow, SetForegroundWindow; import 'focus_window_platform_interface.dart'; @@ -10,45 +11,37 @@ import 'focus_window_platform_interface.dart'; class MethodChannelFocusWindow extends FocusWindowPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - final methodChannel = const MethodChannel('focus_window'); + late final PlatformActivityObserverInterface activityObserver; + + MethodChannelFocusWindow() { + if (Platform.isMacOS) { + activityObserver = const MacosActivityObserver(); + } else if (Platform.isWindows) { + activityObserver = const WindowsActivityObserver(); + } else { + activityObserver = NotSupportedPlatformActivityObserver(); + } + } @override Future getActiveWindowId() async { - if (Platform.isWindows) { - final id = GetForegroundWindow(); - return id; - } else if (Platform.isMacOS) { - final id = await methodChannel.invokeMethod('getActiveWindowId'); - return id; - } - return -1; + return activityObserver.getActiveWindowId(); } @override Future setActiveWindowId(int windowId) async { - if (Platform.isWindows) { - SetForegroundWindow(windowId); - } else if (Platform.isMacOS) { - await methodChannel.invokeMethod( - 'setActiveWindowId', - { - "windowId": windowId, - }, - ); - } + await activityObserver.setActiveWindowId(windowId); return; } @override Future pasteContent() async { - if (Platform.isWindows) { - simulateWindowsPasteShortcut(); - } - if (Platform.isMacOS) { - await methodChannel.invokeMethod("pasteContent", {}); - } - if (Platform.isLinux) { - await methodChannel.invokeMethod("pasteContent", {}); - } + await activityObserver.pasteContent(); } + + @override + Stream get events => activityObserver.events; + + @override + Future get isObserving => activityObserver.isObserving; } diff --git a/plugins/focus_window/lib/focus_window_platform_interface.dart b/plugins/focus_window/lib/focus_window_platform_interface.dart index 2487f79a..7450a0ec 100644 --- a/plugins/focus_window/lib/focus_window_platform_interface.dart +++ b/plugins/focus_window/lib/focus_window_platform_interface.dart @@ -1,8 +1,13 @@ +import 'dart:typed_data'; + +import 'package:focus_window/activity_info.dart'; +import 'package:focus_window/platform_activity_observer_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'focus_window_method_channel.dart'; -abstract class FocusWindowPlatform extends PlatformInterface { +abstract class FocusWindowPlatform extends PlatformInterface + implements PlatformActivityObserverInterface { /// Constructs a FocusWindowPlatform. FocusWindowPlatform() : super(token: _token); @@ -34,4 +39,48 @@ abstract class FocusWindowPlatform extends PlatformInterface { Future pasteContent() { throw UnimplementedError("pasteContent() has not been implemented"); } + + @override + Future getIcon(String applicationPath) { + throw UnimplementedError("getIcon() has not been implemented"); + } + + @override + Future getActivity({bool withIcon = false}) { + throw UnimplementedError("ActivityInfo() has not been implemented"); + } + + @override + Future requestAccessibilityPermission() { + throw UnimplementedError( + "requestAccessibilityPermission() has not been implemented", + ); + } + + @override + Future isAccessibilityPermissionGranted() { + throw UnimplementedError( + "isAccessibilityPermissionGranted() has not been implemented", + ); + } + + @override + Future startObserver() { + throw UnimplementedError( + "isAccessibilityPermissionGranted() has not been implemented", + ); + } + + @override + Future stopObserver() { + throw UnimplementedError( + "isAccessibilityPermissionGranted() has not been implemented", + ); + } + + @override + Future get isObserving; + + @override + Stream get events; } diff --git a/plugins/focus_window/lib/macos.dart b/plugins/focus_window/lib/macos.dart new file mode 100644 index 00000000..c2502549 --- /dev/null +++ b/plugins/focus_window/lib/macos.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:focus_window/activity_info.dart'; +import 'package:focus_window/platform_activity_observer_interface.dart'; + +// exceptions: +class AssistiveAccessNotGranted implements Exception { + final String message; + AssistiveAccessNotGranted() : message = 'Assistive Access not granted'; + + @override + String toString() => 'AssistiveAccessNotGranted(message: $message)'; +} + +// service +class MacosActivityObserver implements PlatformActivityObserverInterface { + const MacosActivityObserver(); + + final EventChannel _channelStream = const EventChannel('focus_window_stream'); + final MethodChannel _channel = const MethodChannel('focus_window'); + + @override + Stream get events => _channelStream.receiveBroadcastStream(); + + @override + Future startObserver() async { + await _channel.invokeMethod("startObserver"); + } + + @override + Future stopObserver() async { + await _channel.invokeMethod("stopObserver"); + } + + @override + Future get isObserving async => + await _channel.invokeMethod("isObserving") as bool; + + @override + Future getIcon(String applicationPath) async { + final result = await _channel.invokeMethod('getIcon', { + 'applicationPath': applicationPath, + }); + + return result; + } + + @override + Future requestAccessibilityPermission() async { + return await _channel.invokeMethod('requestAccessibilityPermission') ?? + false; + } + + Future openAccessibilityPermissionSetting() async { + return await _channel.invokeMethod('openAccessibilityPermissionSetting') ?? + false; + } + + @override + Future isAccessibilityPermissionGranted() async { + if (!kIsWeb && Platform.isMacOS) { + return await _channel.invokeMethod('isAccessibilityPermissionGranted'); + } + return false; + } + + @override + Future getActivity({bool withIcon = false}) async { + final result = await _channel.invokeMethod('getActivity', { + "withIcon": withIcon, + }); + + final macActivity = ActivityInfo.fromMap(Map.from(result)); + return macActivity; + } + + @override + Future getActiveWindowId() async { + final id = await _channel.invokeMethod('getActiveWindowId'); + return id; + } + + @override + Future setActiveWindowId(int windowId) async { + await _channel.invokeMethod( + 'setActiveWindowId', + { + "windowId": windowId, + }, + ); + } + + @override + Future pasteContent() async { + await _channel.invokeMethod("pasteContent", {}); + } +} diff --git a/plugins/focus_window/lib/not_supported.dart b/plugins/focus_window/lib/not_supported.dart new file mode 100644 index 00000000..74bf7871 --- /dev/null +++ b/plugins/focus_window/lib/not_supported.dart @@ -0,0 +1,58 @@ +import 'dart:typed_data'; + +import 'package:focus_window/activity_info.dart'; +import 'package:focus_window/platform_activity_observer_interface.dart'; + +class NotSupportedPlatformActivityObserver + implements PlatformActivityObserverInterface { + @override + Stream get events => throw UnimplementedError(); + + @override + Future getActivity({bool withIcon = false}) { + throw UnimplementedError(); + } + + @override + Future getIcon(String applicationPath) { + return Future.value(null); + } + + @override + Future isAccessibilityPermissionGranted() { + return Future.value(false); + } + + @override + Future get isObserving => Future.value(false); + + @override + Future requestAccessibilityPermission() { + return Future.value(true); + } + + @override + Future startObserver() { + return Future.value(null); + } + + @override + Future stopObserver() { + return Future.value(null); + } + + @override + Future getActiveWindowId() { + return Future.value(-1); + } + + @override + Future pasteContent() { + return Future.value(null); + } + + @override + Future setActiveWindowId(int windowId) { + return Future.value(null); + } +} diff --git a/plugins/focus_window/lib/platform_activity_observer_interface.dart b/plugins/focus_window/lib/platform_activity_observer_interface.dart new file mode 100644 index 00000000..4d612dc2 --- /dev/null +++ b/plugins/focus_window/lib/platform_activity_observer_interface.dart @@ -0,0 +1,18 @@ +import 'dart:typed_data'; + +import 'activity_info.dart'; + +abstract class PlatformActivityObserverInterface { + Future getIcon(String applicationPath); + Future getActivity({bool withIcon = false}); + Future requestAccessibilityPermission(); + Future isAccessibilityPermissionGranted(); + Future startObserver(); + Future stopObserver(); + Future getActiveWindowId(); + Future setActiveWindowId(int windowId); + Future pasteContent(); + Future get isObserving; + + Stream get events; +} diff --git a/plugins/focus_window/lib/utils.dart b/plugins/focus_window/lib/utils.dart new file mode 100644 index 00000000..1b2377d7 --- /dev/null +++ b/plugins/focus_window/lib/utils.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +Future shellOut(String command) async { + final result = await Process.run( + Platform.environment['SHELL'] ?? "/bin/bash", ['-c', command]); + return result.stdout.toString(); +} + +Future runInPowershell(List commands) async { + final result = await Process.run( + 'powershell.exe', + ['-ExecutionPolicy', 'Bypass', ...commands], + ); + if (kDebugMode) { + print(result.stderr); + } + return result.stdout.toString(); +} diff --git a/plugins/focus_window/lib/windows.dart b/plugins/focus_window/lib/windows.dart new file mode 100644 index 00000000..a9fe5c31 --- /dev/null +++ b/plugins/focus_window/lib/windows.dart @@ -0,0 +1,158 @@ +// service +import 'dart:async'; +import 'dart:convert' show base64; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:focus_window/activity_info.dart'; +import 'package:focus_window/utils.dart'; +import 'package:focus_window/windows_paste_simulator.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:win32/win32.dart'; + +import 'platform_activity_observer_interface.dart'; + +final _cache = {}; +ActivityInfo? _lastActivity; + +class WindowsActivityObserver implements PlatformActivityObserverInterface { + const WindowsActivityObserver(); + + Future cacheAll() async { + final futures = []; + + if (!_cache.containsKey('GetActivity')) { + futures.add( + rootBundle + .loadString('packages/tracker_libs/windows_utils/Get-Activity.ps1') + .then((value) => _cache['GetActivity'] = value), + ); + } + if (!_cache.containsKey('GetIcon')) { + futures.add( + rootBundle + .loadString('plugins/focus_window/windows_utils/Get-Icon.ps1') + .then((value) async { + final tempDir = await getTemporaryDirectory(); + final file = File(p.join(tempDir.path, '._gi.ps1')); + await file.writeAsString(value); + _cache['GetIcon'] = file.path; + }), + ); + } + if (!_cache.containsKey('GetUrl')) { + futures.add( + rootBundle + .loadString('packages/tracker_libs/windows_utils/Get-Url.ps1') + .then((value) async { + final tempDir = await getTemporaryDirectory(); + final file = File(p.join(tempDir.path, '._gu.ps1')); + await file.writeAsString(value); + _cache['GetUrl'] = file.path; + }), + ); + } + + if (!_cache.containsKey('GetScreenshot')) { + futures.add( + rootBundle + .loadString( + 'packages/tracker_libs/windows_utils/Get-Screenshot.ps1') + .then((value) => _cache['GetScreenshot'] = value), + ); + } + + await Future.wait(futures); + } + + @override + Future getIcon(String applicationPath) async { + final result = await runInPowershell([ + '-File', + _cache['GetIcon']!, + '-ApplicationPath', + applicationPath, + ]); + return base64.decode(result.trim()); + } + + Future getUrl(String app, String windowTitle) async { + throw UnimplementedError(); + + // final result = await runInPowershell( + // [ + // _cache['GetUrl']!, + // '-app', + // app, + // '-windowTitle', + // "'$windowTitle'", + // ], + // ); + // return result; + } + + @override + Future getActivity({bool withIcon = false}) async { + final result = await runInPowershell([ + _cache['GetActivity']!, + ]); + + var activity = ActivityInfo.fromJson(result); + + if (activity.title != _lastActivity?.title) { + final url = await getUrl(activity.appFileName, activity.title); + activity = activity.copyWith(url: url); + } + + _lastActivity = activity; + + if (withIcon) { + final icon = await getIcon(_lastActivity!.appFilePath); + _lastActivity = _lastActivity!.copyWith(icon: icon); + } + return _lastActivity!; + } + + @override + Future isAccessibilityPermissionGranted() async { + return true; + } + + @override + Future requestAccessibilityPermission() async { + return true; + } + + @override + Stream get events => throw UnimplementedError(); + + @override + Future get isObserving => throw UnimplementedError(); + + @override + Future startObserver() { + throw UnimplementedError(); + } + + @override + Future stopObserver() { + throw UnimplementedError(); + } + + @override + Future getActiveWindowId() async { + final id = GetForegroundWindow(); + return id; + } + + @override + Future setActiveWindowId(int windowId) async { + SetForegroundWindow(windowId); + } + + @override + Future pasteContent() async { + simulateWindowsPasteShortcut(); + } +} diff --git a/plugins/focus_window/macos/Classes/EventCallback.swift b/plugins/focus_window/macos/Classes/EventCallback.swift new file mode 100644 index 00000000..a8adbe57 --- /dev/null +++ b/plugins/focus_window/macos/Classes/EventCallback.swift @@ -0,0 +1,29 @@ +// +// EventCallback.swift +// tracker_libs +// +// Created by Raj Singh on 27/08/2024. +// + +import Foundation + +public class WindowChanged: NSObject { + private var oldWindow: AXUIElement? + + @objc public func focusedWindowChanged(_ observer: AXObserver, window: AXUIElement) { + if oldWindow != nil { + AXObserverRemoveNotification( + observer, oldWindow!, kAXFocusedWindowChangedNotification as CFString) + } + + let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + AXObserverAddNotification(observer, window, kAXTitleChangedNotification as CFString, selfPtr) + + let event: Dictionary = [ + "type" : "WindowChanged" + ] + try? TrackerLibPlugin.eventChannel?.success(event: event) + + oldWindow = window + } +} \ No newline at end of file diff --git a/plugins/focus_window/macos/Classes/EventManager.swift b/plugins/focus_window/macos/Classes/EventManager.swift new file mode 100644 index 00000000..537cdba3 --- /dev/null +++ b/plugins/focus_window/macos/Classes/EventManager.swift @@ -0,0 +1,43 @@ +// +// EventManager.swift +// tracker_libs +// +// Created by Raj Singh on 27/08/2024. +// + +import Foundation +import FlutterMacOS + +public class EventChannelHandler: NSObject, FlutterStreamHandler { + + private var eventSink: FlutterEventSink? + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + return nil + } + + public init(name: String, messenger: FlutterBinaryMessenger) { + super.init() + let eventChannel = FlutterEventChannel(name: name, binaryMessenger: messenger) + eventChannel.setStreamHandler(self) + } + + + public func success(event: Any?) throws { + if eventSink != nil { + eventSink?(event) + } + } + + public func error(code: String, message: String?, details: Any? = nil) { + if eventSink != nil { + eventSink?(FlutterError(code: code, message: message, details: details)) + } + } +} \ No newline at end of file diff --git a/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift b/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift index f0b9de11..ef6f0c43 100644 --- a/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift +++ b/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift @@ -1,9 +1,45 @@ import Cocoa import FlutterMacOS +import AppKit +import AVFoundation +import Security +import ScriptingBridge + + +@objc protocol ChromeTab { + @objc optional var URL: String { get } + @objc optional var title: String { get } +} + +@objc protocol ChromeWindow { + @objc optional var activeTab: ChromeTab { get } + @objc optional var mode: String { get } +} + +extension SBObject: ChromeWindow, ChromeTab {} + +@objc protocol ChromeProtocol { + @objc optional func windows() -> [ChromeWindow] +} + +extension SBApplication: ChromeProtocol {} + public class FocusWindowPlugin: NSObject, FlutterPlugin { + private var observer: AXObserver? + private var eventListening = false + public static var windowChangedCallback: WindowChanged = WindowChanged() + public static var eventChannel: EventChannelHandler? + + + public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "focus_window", binaryMessenger: registrar.messenger) + let channel = FlutterMethodChannel(name: "focus_window", + binaryMessenger: registrar.messenger) + eventChannel = EventChannelHandler( + name: "focus_window_stream", + messenger: registrar.messenger + ) let instance = FocusWindowPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } @@ -61,8 +97,273 @@ public class FocusWindowPlugin: NSObject, FlutterPlugin { case "pasteContent": self.pasteContent() result(nil) + // IMPL + case "isAccessibilityPermissionGranted": + isAccessibilityPermissionGranted(call, result: result) + break + case "requestAccessibilityPermission": + requestAccessibilityPermission(call, result: result) + break + case "openAccessibilityPermissionSetting": + openAccessibilityPermissionSetting(call, result: result) + break + case "getIcon": + getIcon(call, result: result) + break + case "getActivity": + getActivity(call, result: result) + break + case "startObserver": + startListening() + break + case "stopObserver": + stopListening() + break + case "isObserving": + isListening(call, result: result) + break default: result(FlutterMethodNotImplemented) } } + + // Utilities + @discardableResult + private func runAppleScript(source: String?) -> String? { + if (source == nil || source == ""){ + return nil; + } + var error: NSDictionary? + if let result = NSAppleScript(source: source!) { + let output = result.executeAndReturnError(&error).stringValue + return output + } + if (error != nil) { + print(error!); + } + return nil + } + + // Private + + private func getIconForApplicationPath(_ applicationPath: String) -> NSImage? { + let application = NSWorkspace.shared.icon(forFile: applicationPath) + return application + } + + private func getFrontApp() -> NSRunningApplication? { + return NSWorkspace.shared.frontmostApplication; + } + + private func getUrlForChromiumBasedBrowser(_ appId: String) -> String? { + + switch appId { + case "com.google.Chrome", "com.google.Chrome.beta", "com.google.Chrome.dev", "com.google.Chrome.canary", "com.brave.Browser", "com.brave.Browser.beta", "com.brave.Browser.nightly", "com.microsoft.edgemac", "com.microsoft.edgemac.Beta", "com.microsoft.edgemac.Dev", "com.microsoft.edgemac.Canary", "com.mighty.app", "com.ghostbrowser.gb1", "com.bookry.wavebox", "com.pushplaylabs.sidekick", "com.vivaldi.Vivaldi": + + let chromeObject: ChromeProtocol = SBApplication.init(bundleIdentifier: appId)! + + let frontWindow = chromeObject.windows?()[0] + let activeTab = frontWindow?.activeTab + return activeTab?.URL + default: + return nil + } + + } + + private func getActiveBrowserTabURLAppleScriptCommand(_ appId: String) -> String? { + switch appId { + case "com.google.Chrome", "com.google.Chrome.beta", "com.google.Chrome.dev", "com.google.Chrome.canary", "com.brave.Browser", "com.brave.Browser.beta", "com.brave.Browser.nightly", "com.microsoft.edgemac", "com.microsoft.edgemac.Beta", "com.microsoft.edgemac.Dev", "com.microsoft.edgemac.Canary", "com.mighty.app", "com.ghostbrowser.gb1", "com.bookry.wavebox", "com.pushplaylabs.sidekick", "com.operasoftware.Opera", "com.operasoftware.OperaNext", "com.operasoftware.OperaDeveloper", "com.vivaldi.Vivaldi": + return "tell app id \"\(appId)\" to get the URL of active tab of front window" + case "com.apple.Safari", "com.apple.SafariTechnologyPreview": + return "tell app id \"\(appId)\" to get URL of front document" + default: + return nil + } + } + + public func getActivity(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + let args:[String: Any?] = call.arguments as! [String: Any?]; + let withIcon: Bool = (args["withIcon"] ?? false) as! Bool ; + + var activity: [String: Any?] = [ + "pid": -1, + "app": "", + "appFileName": "", + "appFilePath": "", + "identifier": "", + "title": "", + "url": "", + "document": "", + "icon": nil, + ] + + guard + let application = getFrontApp(), + let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] + else { + return + } + + let frontmostAppPID = application.processIdentifier; + +// Getting the opened document + var elements = [AXUIElement]() + var windowList: AnyObject? = nil + let appRef = AXUIElementCreateApplication(frontmostAppPID) + if AXUIElementCopyAttributeValue(appRef, "AXWindows" as CFString, &windowList) == .success { + elements = windowList as! [AXUIElement] + } + + var docRef: AnyObject? = nil + if AXUIElementCopyAttributeValue(elements.first!, "AXDocument" as CFString, &docRef) == .success { + let filePath = docRef as! String + activity["document"] = filePath + } + +// Getting the url + var url: String? + + if (application.bundleIdentifier != nil) { + url = getUrlForChromiumBasedBrowser(application.bundleIdentifier!) + if (url == nil) { + let script = getActiveBrowserTabURLAppleScriptCommand(application.bundleIdentifier ?? ""); + url = runAppleScript(source: script ?? ""); + } + } + + for window in windows { + let windowOwnerPID = window[kCGWindowOwnerPID as String] as! pid_t + if windowOwnerPID != frontmostAppPID { + continue + } + if (window[kCGWindowAlpha as String] as! Double) == 0 { + continue + } + + let windowTitle = window[kCGWindowName as String] as? String ?? "" + activity["title"] = windowTitle + } + if (withIcon && application.icon != nil) { + let icon = NSBitmapImageRep(data: application.icon!.tiffRepresentation(using: .lzw, factor: .greatestFiniteMagnitude)!)!.representation(using: .png, properties: [:]); + activity["icon"] = icon; + } + + activity["pid"] = application.processIdentifier; + activity["app"] = application.localizedName; + activity["appFileName"] = application.bundleURL?.lastPathComponent; + activity["appFilePath"] = application.bundleURL?.path; + activity["identifier"] = application.bundleIdentifier; + activity["url"] = url; + + result(activity) + } + + public func getIcon(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args:[String: Any] = call.arguments as! [String: Any] + let applicationPath: String = args["applicationPath"] as! String + let application = getIconForApplicationPath(applicationPath) + if (application != nil) { + let data = NSBitmapImageRep(data: application!.tiffRepresentation(using: .lzw, factor: .greatestFiniteMagnitude)!)!.representation(using: .png, properties: [:]); + + if (data != nil) { + result(data) + } + } + } + + public func isAccessibilityPermissionGranted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let isGranted: Bool = AXIsProcessTrusted() + result(isGranted) + } + + public func requestAccessibilityPermission(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString: true] + let accessGranted: Bool = AXIsProcessTrustedWithOptions(options) + result(accessGranted) + } + + public func openAccessibilityPermissionSetting(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let opened = NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" + )! + ) + result(opened) + } + + // OBSERVERS + + public func isListening(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result(eventListening) + } + + public func startListening() { + if (!eventListening) + { + NSWorkspace.shared.notificationCenter.addObserver( + self, selector: #selector(focusedAppChanged), + name: NSWorkspace.didActivateApplicationNotification, + object: nil + ) + eventListening = true + } + } + + public func stopListening() { + if (eventListening) { + NSWorkspace.shared.notificationCenter.removeObserver(NSWorkspace.didActivateApplicationNotification) + eventListening = false + } + } + + + @objc public func focusedAppChanged() { + if observer != nil { + CFRunLoopRemoveSource( + RunLoop.current.getCFRunLoop(), + AXObserverGetRunLoopSource(observer!), + CFRunLoopMode.defaultMode) + } + + let frontmost = NSWorkspace.shared.frontmostApplication! + let pid = frontmost.processIdentifier + let focusedApp = AXUIElementCreateApplication(pid) + + AXObserverCreate( + pid, + { + ( + _ axObserver: AXObserver, + axElement: AXUIElement, + notification: CFString, + userData: UnsafeMutableRawPointer? + ) -> Void in + + if notification == kAXFocusedWindowChangedNotification as CFString { + TrackerLibPlugin.windowChangedCallback.focusedWindowChanged(axObserver, window: axElement) + } else { + let event: Dictionary = [ + "type": "TabChanged", + ] + try? TrackerLibPlugin.eventChannel?.success(event: event) + } + }, &observer) + + let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + AXObserverAddNotification( + observer!, focusedApp, kAXFocusedWindowChangedNotification as CFString, selfPtr) + + CFRunLoopAddSource( + RunLoop.current.getCFRunLoop(), + AXObserverGetRunLoopSource(observer!), + CFRunLoopMode.defaultMode) + + var focusedWindow: AnyObject? + AXUIElementCopyAttributeValue(focusedApp, kAXFocusedWindowAttribute as CFString, &focusedWindow) + + if focusedWindow != nil { + TrackerLibPlugin.windowChangedCallback.focusedWindowChanged(observer!, window: focusedWindow as! AXUIElement) + } + } } diff --git a/plugins/focus_window/pubspec.yaml b/plugins/focus_window/pubspec.yaml index e16b8b86..e69105c9 100644 --- a/plugins/focus_window/pubspec.yaml +++ b/plugins/focus_window/pubspec.yaml @@ -14,6 +14,9 @@ dependencies: win32: ^5.5.4 universal_io: ^2.2.2 ffi: ^2.1.2 + equatable: ^2.0.5 + path_provider: ^2.1.4 + path: ^1.9.0 dev_dependencies: flutter_test: diff --git a/plugins/focus_window/window_utils/GetActivity.ps1 b/plugins/focus_window/window_utils/GetActivity.ps1 new file mode 100644 index 00000000..7b105d5e --- /dev/null +++ b/plugins/focus_window/window_utils/GetActivity.ps1 @@ -0,0 +1,33 @@ +Add-Type @" + using System; + using System.Runtime.InteropServices; + using System.Text; + + public class APIFuncs + { + [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] + public static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll")] + public static extern Int32 GetWindowThreadProcessId(IntPtr hWnd,out Int32 lpdwProcessId); + + public static Int32 GetWindowProcessID(IntPtr hwnd) + { + Int32 pid; + GetWindowThreadProcessId(hwnd, out pid); + return pid; + } + } +"@ + +Add-Type -AssemblyName System.Drawing + +$w = [APIFuncs]::GetForegroundWindow() +$p = [APIFuncs]::GetWindowProcessID($w) + + +$WH = get-process | Where-Object { $_.Id -eq $p } + +@" +Company,CPU,Description,FileVersion,Id,MainWindowTitle,Name,Path,ProcessName,Product,StartTime +$($WH.Company),$($WH.CPU),$($WH.Description),$($WH.FileVersion),$($WH.Id),$($WH.MainWindowTitle),$($WH.Name),$($WH.Path),$($WH.ProcessName),$($WH.Product),$($WH.MainWindowHandle) +"@ | ConvertFrom-Csv | ConvertTo-Json \ No newline at end of file diff --git a/plugins/focus_window/window_utils/GetIcon.ps1 b/plugins/focus_window/window_utils/GetIcon.ps1 new file mode 100644 index 00000000..cdcef431 --- /dev/null +++ b/plugins/focus_window/window_utils/GetIcon.ps1 @@ -0,0 +1,15 @@ +param ( + [Parameter(Mandatory)][string]$ApplicationPath +) + +Add-Type -AssemblyName System.Drawing +$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($ApplicationPath).toBitmap() +[System.Drawing.Imaging.ImageFormat]$ImageFormat = [System.Drawing.Imaging.ImageFormat]::Png +$memory = New-Object System.IO.MemoryStream +$icon.Save($memory, $ImageFormat) + +[byte[]]$bytes = $memory.ToArray() +$memory.Flush() +$memory.Dispose() + +[System.Convert]::ToBase64String($bytes) \ No newline at end of file diff --git a/plugins/focus_window/window_utils/GetUrl.ps1 b/plugins/focus_window/window_utils/GetUrl.ps1 new file mode 100644 index 00000000..05bc399c --- /dev/null +++ b/plugins/focus_window/window_utils/GetUrl.ps1 @@ -0,0 +1,59 @@ +param ( + [string]$app, + [string]$windowTitle +) + +Add-Type -AssemblyName System.Windows.Forms + +$selcetKey = "{F4}" +$deseclectKey = "{F6}" + +if ($app -eq "msedge") { + $selcetKey = "{F4}" + $deseclectKey = "{F6}" +} +elseif ($app -eq "chrome") { + $selcetKey = "%d" + $deseclectKey = "+{F6}" +} +elseif ($app -eq "firefox") { + $selcetKey = "{F6}" + $deseclectKey = "+{F6}" +} +elseif ($app -eq "brave") { + $selcetKey = "%{d}" #alt + d + $deseclectKey = "+{F6}" +} +else { + # not supported platform + return +} + +$lastVal = Get-Clipboard -Raw +$wshell = New-Object -ComObject wscript.shell +$Null = $wshell.AppActivate("AppActivate") +Set-Clipboard -Value $null +$address = $null +$count = 1 +# Write-Host "Pressing $selcetKey" +[System.Windows.Forms.SendKeys]::SendWait($selcetKey) +do { + if ($count % 2 -eq 0) { + [System.Windows.Forms.SendKeys]::SendWait($selcetKey) + # Write-Host "Pressing $selcetKey" + } + # Write-Host "Pressing ^c" + [System.Windows.Forms.SendKeys]::SendWait("^c") + $address = Get-Clipboard + $count = $count + 1 + Start-Sleep -Milliseconds 1 +} +while ($null -eq $address -And $count -lt 10) +Set-Clipboard -Value $lastVal +# Write-Host "Pressing $deseclectKey" +[System.Windows.Forms.SendKeys]::SendWait($deseclectKey) +Remove-Variable wshell +Remove-Variable lastVal +$BrowserUrl = $address + +Write-Host $BrowserUrl \ No newline at end of file From 2c5238ebaaa37445fdcce0149d2c8f279f74255f Mon Sep 17 00:00:00 2001 From: Raj Date: Wed, 28 Aug 2024 11:02:05 +0530 Subject: [PATCH 02/15] WIP: active activity detection --- lib/widgets/window_focus_manager.dart | 21 ++++++++++ packages/copycat_base | 2 +- plugins/focus_window/lib/focus_window.dart | 32 +++++++++++++++ .../lib/focus_window_method_channel.dart | 39 +++++++++++++++++-- .../lib/focus_window_platform_interface.dart | 7 +++- .../lib/{ => platform}/activity_info.dart | 0 .../lib/{ => platform}/macos.dart | 7 ++-- .../lib/{ => platform}/not_supported.dart | 4 +- .../platform_activity_observer_interface.dart | 0 .../lib/{ => platform}/utils.dart | 0 .../lib/{ => platform}/windows.dart | 6 +-- .../windows_paste_simulator.dart | 0 .../macos/Classes/EventCallback.swift | 4 +- .../macos/Classes/FocusWindowPlugin.swift | 8 ++-- pubspec.lock | 12 +++++- 15 files changed, 119 insertions(+), 23 deletions(-) rename plugins/focus_window/lib/{ => platform}/activity_info.dart (100%) rename plugins/focus_window/lib/{ => platform}/macos.dart (91%) rename plugins/focus_window/lib/{ => platform}/not_supported.dart (88%) rename plugins/focus_window/lib/{ => platform}/platform_activity_observer_interface.dart (100%) rename plugins/focus_window/lib/{ => platform}/utils.dart (100%) rename plugins/focus_window/lib/{ => platform}/windows.dart (95%) rename plugins/focus_window/lib/{ => platform}/windows_paste_simulator.dart (100%) diff --git a/lib/widgets/window_focus_manager.dart b/lib/widgets/window_focus_manager.dart index f7b2fd58..a9cb7c25 100644 --- a/lib/widgets/window_focus_manager.dart +++ b/lib/widgets/window_focus_manager.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:clipboard/di/di.dart'; import 'package:clipboard/utils/clipboard_actions.dart'; import 'package:clipboard/utils/utility.dart'; @@ -40,6 +42,7 @@ class WindowFocusManager extends StatefulWidget { class WindowFocusManagerState extends State with WindowListener { int? lastWindowId; + StreamSubscription? subscription; late final AppConfigCubit appConfigCubit; @@ -111,16 +114,34 @@ class WindowFocusManagerState extends State appConfigCubit.setLastFocusedWindowId(lastWindowId); } + void onFocuswindowChange(data) { + print(data); + } + + void startListners() async { + // final hasGrant = + // await widget.focusWindow.isAccessibilityPermissionGranted(); + // print("GRANTED: $hasGrant"); + + // final activity = await widget.focusWindow.getActivity(); + // await widget.focusWindow.startObserver(); + // print("OBSERVING: ${widget.focusWindow.isObserving}"); + // subscription ??= widget.focusWindow.events.listen(onFocuswindowChange); + } + @override void initState() { super.initState(); windowManager.addListener(this); windowManager.setPreventClose(true); appConfigCubit = context.read(); + startListners(); } @override void dispose() { + subscription?.cancel(); + widget.focusWindow.stopObserver(); windowManager.removeListener(this); super.dispose(); } diff --git a/packages/copycat_base b/packages/copycat_base index c745857c..3941d574 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit c745857cc1d1e6d54acfb9c34e681101680a8527 +Subproject commit 3941d574dfafe39200134fa2b3be1fbfff5319d1 diff --git a/plugins/focus_window/lib/focus_window.dart b/plugins/focus_window/lib/focus_window.dart index 90bccc80..abc56915 100644 --- a/plugins/focus_window/lib/focus_window.dart +++ b/plugins/focus_window/lib/focus_window.dart @@ -1,3 +1,7 @@ +import 'dart:typed_data'; + +import 'package:focus_window/platform/activity_info.dart'; + import 'focus_window_platform_interface.dart'; class FocusWindow { @@ -12,4 +16,32 @@ class FocusWindow { Future pasteContent() { return FocusWindowPlatform.instance.pasteContent(); } + + Stream get events => FocusWindowPlatform.instance.events; + + Future getActivity({bool withIcon = false}) { + return FocusWindowPlatform.instance.getActivity(withIcon: withIcon); + } + + Future getIcon(String applicationPath) { + return FocusWindowPlatform.instance.getIcon(applicationPath); + } + + Future isAccessibilityPermissionGranted() { + return FocusWindowPlatform.instance.isAccessibilityPermissionGranted(); + } + + Future get isObserving => FocusWindowPlatform.instance.isObserving; + + Future requestAccessibilityPermission() { + return FocusWindowPlatform.instance.requestAccessibilityPermission(); + } + + Future startObserver() { + return FocusWindowPlatform.instance.startObserver(); + } + + Future stopObserver() { + return FocusWindowPlatform.instance.stopObserver(); + } } diff --git a/plugins/focus_window/lib/focus_window_method_channel.dart b/plugins/focus_window/lib/focus_window_method_channel.dart index 1c863f7e..bbe6c336 100644 --- a/plugins/focus_window/lib/focus_window_method_channel.dart +++ b/plugins/focus_window/lib/focus_window_method_channel.dart @@ -1,8 +1,9 @@ import 'package:flutter/foundation.dart'; -import 'package:focus_window/macos.dart'; -import 'package:focus_window/not_supported.dart'; -import 'package:focus_window/platform_activity_observer_interface.dart'; -import 'package:focus_window/windows.dart'; +import 'package:focus_window/platform/activity_info.dart'; +import 'package:focus_window/platform/macos.dart'; +import 'package:focus_window/platform/not_supported.dart'; +import 'package:focus_window/platform/platform_activity_observer_interface.dart'; +import 'package:focus_window/platform/windows.dart'; import 'package:universal_io/io.dart'; import 'focus_window_platform_interface.dart'; @@ -44,4 +45,34 @@ class MethodChannelFocusWindow extends FocusWindowPlatform { @override Future get isObserving => activityObserver.isObserving; + + @override + Future getActivity({bool withIcon = false}) { + return activityObserver.getActivity(withIcon: withIcon); + } + + @override + Future getIcon(String applicationPath) { + return activityObserver.getIcon(applicationPath); + } + + @override + Future isAccessibilityPermissionGranted() { + return activityObserver.isAccessibilityPermissionGranted(); + } + + @override + Future requestAccessibilityPermission() { + return activityObserver.requestAccessibilityPermission(); + } + + @override + Future startObserver() { + return activityObserver.startObserver(); + } + + @override + Future stopObserver() { + return activityObserver.stopObserver(); + } } diff --git a/plugins/focus_window/lib/focus_window_platform_interface.dart b/plugins/focus_window/lib/focus_window_platform_interface.dart index 7450a0ec..9f87bf41 100644 --- a/plugins/focus_window/lib/focus_window_platform_interface.dart +++ b/plugins/focus_window/lib/focus_window_platform_interface.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; -import 'package:focus_window/activity_info.dart'; -import 'package:focus_window/platform_activity_observer_interface.dart'; +import 'package:focus_window/platform/activity_info.dart'; +import 'package:focus_window/platform/platform_activity_observer_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'focus_window_method_channel.dart'; @@ -28,14 +28,17 @@ abstract class FocusWindowPlatform extends PlatformInterface _instance = instance; } + @override Future getActiveWindowId() { throw UnimplementedError('getActiveWindowId() has not been implemented.'); } + @override Future setActiveWindowId(int windowId) { throw UnimplementedError('setActiveWindowId() has not been implemented.'); } + @override Future pasteContent() { throw UnimplementedError("pasteContent() has not been implemented"); } diff --git a/plugins/focus_window/lib/activity_info.dart b/plugins/focus_window/lib/platform/activity_info.dart similarity index 100% rename from plugins/focus_window/lib/activity_info.dart rename to plugins/focus_window/lib/platform/activity_info.dart diff --git a/plugins/focus_window/lib/macos.dart b/plugins/focus_window/lib/platform/macos.dart similarity index 91% rename from plugins/focus_window/lib/macos.dart rename to plugins/focus_window/lib/platform/macos.dart index c2502549..f64cca0c 100644 --- a/plugins/focus_window/lib/macos.dart +++ b/plugins/focus_window/lib/platform/macos.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:focus_window/activity_info.dart'; -import 'package:focus_window/platform_activity_observer_interface.dart'; +import 'package:focus_window/platform/activity_info.dart'; +import 'package:focus_window/platform/platform_activity_observer_interface.dart'; // exceptions: class AssistiveAccessNotGranted implements Exception { @@ -27,7 +27,8 @@ class MacosActivityObserver implements PlatformActivityObserverInterface { @override Future startObserver() async { - await _channel.invokeMethod("startObserver"); + final result = await _channel.invokeMethod("startObserver"); + print(result); } @override diff --git a/plugins/focus_window/lib/not_supported.dart b/plugins/focus_window/lib/platform/not_supported.dart similarity index 88% rename from plugins/focus_window/lib/not_supported.dart rename to plugins/focus_window/lib/platform/not_supported.dart index 74bf7871..5a9d7d5c 100644 --- a/plugins/focus_window/lib/not_supported.dart +++ b/plugins/focus_window/lib/platform/not_supported.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; -import 'package:focus_window/activity_info.dart'; -import 'package:focus_window/platform_activity_observer_interface.dart'; +import 'package:focus_window/platform/activity_info.dart'; +import 'package:focus_window/platform/platform_activity_observer_interface.dart'; class NotSupportedPlatformActivityObserver implements PlatformActivityObserverInterface { diff --git a/plugins/focus_window/lib/platform_activity_observer_interface.dart b/plugins/focus_window/lib/platform/platform_activity_observer_interface.dart similarity index 100% rename from plugins/focus_window/lib/platform_activity_observer_interface.dart rename to plugins/focus_window/lib/platform/platform_activity_observer_interface.dart diff --git a/plugins/focus_window/lib/utils.dart b/plugins/focus_window/lib/platform/utils.dart similarity index 100% rename from plugins/focus_window/lib/utils.dart rename to plugins/focus_window/lib/platform/utils.dart diff --git a/plugins/focus_window/lib/windows.dart b/plugins/focus_window/lib/platform/windows.dart similarity index 95% rename from plugins/focus_window/lib/windows.dart rename to plugins/focus_window/lib/platform/windows.dart index a9fe5c31..f984c5cb 100644 --- a/plugins/focus_window/lib/windows.dart +++ b/plugins/focus_window/lib/platform/windows.dart @@ -4,9 +4,9 @@ import 'dart:convert' show base64; import 'dart:io'; import 'package:flutter/services.dart'; -import 'package:focus_window/activity_info.dart'; -import 'package:focus_window/utils.dart'; -import 'package:focus_window/windows_paste_simulator.dart'; +import 'package:focus_window/platform/activity_info.dart'; +import 'package:focus_window/platform/utils.dart'; +import 'package:focus_window/platform/windows_paste_simulator.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart'; diff --git a/plugins/focus_window/lib/windows_paste_simulator.dart b/plugins/focus_window/lib/platform/windows_paste_simulator.dart similarity index 100% rename from plugins/focus_window/lib/windows_paste_simulator.dart rename to plugins/focus_window/lib/platform/windows_paste_simulator.dart diff --git a/plugins/focus_window/macos/Classes/EventCallback.swift b/plugins/focus_window/macos/Classes/EventCallback.swift index a8adbe57..14d15b4e 100644 --- a/plugins/focus_window/macos/Classes/EventCallback.swift +++ b/plugins/focus_window/macos/Classes/EventCallback.swift @@ -22,8 +22,8 @@ public class WindowChanged: NSObject { let event: Dictionary = [ "type" : "WindowChanged" ] - try? TrackerLibPlugin.eventChannel?.success(event: event) + try? FocusWindowPlugin.eventChannel?.success(event: event) oldWindow = window } -} \ No newline at end of file +} diff --git a/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift b/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift index ef6f0c43..fe1ac4b2 100644 --- a/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift +++ b/plugins/focus_window/macos/Classes/FocusWindowPlugin.swift @@ -217,7 +217,7 @@ public class FocusWindowPlugin: NSObject, FlutterPlugin { } var docRef: AnyObject? = nil - if AXUIElementCopyAttributeValue(elements.first!, "AXDocument" as CFString, &docRef) == .success { + if !elements.isEmpty && AXUIElementCopyAttributeValue(elements.first!, "AXDocument" as CFString, &docRef) == .success { let filePath = docRef as! String activity["document"] = filePath } @@ -341,12 +341,12 @@ public class FocusWindowPlugin: NSObject, FlutterPlugin { ) -> Void in if notification == kAXFocusedWindowChangedNotification as CFString { - TrackerLibPlugin.windowChangedCallback.focusedWindowChanged(axObserver, window: axElement) + FocusWindowPlugin.windowChangedCallback.focusedWindowChanged(axObserver, window: axElement) } else { let event: Dictionary = [ "type": "TabChanged", ] - try? TrackerLibPlugin.eventChannel?.success(event: event) + try? FocusWindowPlugin.eventChannel?.success(event: event) } }, &observer) @@ -363,7 +363,7 @@ public class FocusWindowPlugin: NSObject, FlutterPlugin { AXUIElementCopyAttributeValue(focusedApp, kAXFocusedWindowAttribute as CFString, &focusedWindow) if focusedWindow != nil { - TrackerLibPlugin.windowChangedCallback.focusedWindowChanged(observer!, window: focusedWindow as! AXUIElement) + FocusWindowPlugin.windowChangedCallback.focusedWindowChanged(observer!, window: focusedWindow as! AXUIElement) } } } diff --git a/pubspec.lock b/pubspec.lock index c0ed9b6b..9b587e35 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -447,6 +447,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -1207,10 +1215,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: From fe62c7a8804bcf08a76191a08189d7160925b21b Mon Sep 17 00:00:00 2001 From: Raj Date: Thu, 29 Aug 2024 11:14:03 +0530 Subject: [PATCH 03/15] fix: incorrect frequenct syncing error message --- packages/copycat_base | 2 +- packages/copycat_pro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/copycat_base b/packages/copycat_base index 3941d574..acf81bd1 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 3941d574dfafe39200134fa2b3be1fbfff5319d1 +Subproject commit acf81bd1302792fb858e50da0b42eaf2c409e17a diff --git a/packages/copycat_pro b/packages/copycat_pro index 25991d99..37b49dfe 160000 --- a/packages/copycat_pro +++ b/packages/copycat_pro @@ -1 +1 @@ -Subproject commit 25991d99375bf40af7f27d3e4544ce3c8d8b96fa +Subproject commit 37b49dfe7af4d9daf7406e8758d80f71e3630888 From 67d7a383863bd79a91d92fd63ddd1a9de6de0d67 Mon Sep 17 00:00:00 2001 From: Raj Date: Thu, 29 Aug 2024 11:35:10 +0530 Subject: [PATCH 04/15] added change log --- ChangeLog.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 ChangeLog.md diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 00000000..6dfcff44 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,74 @@ +## 0.1.23 ( WIP ) + +#### Features + +WIP + +#### Bug Fixes + +- Fix: Incorrect frequenent syncing error message toast when syncing files + +## 0.1.22 + +#### Features + +- 🎉 Drag and Drop ( Desktop and Ipad Os, Android will be coming in upcoming updates ) +- 🎉 Customization and Themings +- 🎉 Advance filters by type, sub types, date range, sorting, etc. ( Feature request ) +- 🎉 New and polished user interface. ( Feature request ) +- 💪 Performance improvements when large amount of clips are synced and decrypted. +- Toggling clipboard by clicking on the menu icon/tray icon. ( Feature request ) +- We fixed alot of bugs which you might have experienced. + +#### Bug Fixes and Improvements + +##### Windows + +- File/Media copying from Explorer app not working as expected. + +## 0.1.21 + +Enhancements Based on our Discord community and Feedback from users. + +#### Critical Changes: + +- Full Local Clipboard: All core features (except Syncing) are now available without requiring sign-in. + +- Local End-to-End Encryption: Enhanced security for all users when using syncing. +- Extensive Keyboard Shortcuts: Faster navigation with new keyboard shortcuts. + - Home - Ctrl/Cmd + D + - Search - Ctrl/Cmd + F + - Collection - Ctrl/Cmd + C + - Settings - Ctrl/Cmd + X + - Paste - Ctrl/Cmd + V + - Sync/Refresh - Ctrl/Cmd + R + - Navigation - Arrow Keys ( ← ↑ → ↓ ) + - Hide/Close/Go Back - Escape + +#### General Changes: + +- Bug fixes and localization improvements. +- UI tweaks for a better user experience. + +## 0.1.20 + +#### Android ( Bug Fixes ) + +- Fix the issue where forget password link sent on email was not working. + +#### MacOS ( Intel • Critical Update ) + +- Intel mac crashing on launch has been fixed. + +## 0.1.19 + +- Improved memory management +- Bug fixes and stability enhancements + +#### Windows/MacOS ( Critical Update ) + +- Critical bug fixes including abnormal memory usage under certain circumstances. + +## 0.1.18 + +- Bug fixes and performance optimization From 1b78265983f2f92380ff5be89363ee9d075f338e Mon Sep 17 00:00:00 2001 From: Raj Date: Sat, 31 Aug 2024 11:58:12 +0530 Subject: [PATCH 05/15] wip: fetch active window details --- lib/pages/settings/widgets/smart_paste_switch.dart | 9 +++++++-- lib/widgets/window_focus_manager.dart | 12 ------------ packages/copycat_base | 2 +- plugins/focus_window/lib/focus_window.dart | 5 +++++ .../lib/focus_window_method_channel.dart | 5 +++++ .../lib/focus_window_platform_interface.dart | 7 +++++++ plugins/focus_window/lib/platform/macos.dart | 6 +++--- plugins/focus_window/lib/platform/not_supported.dart | 5 +++++ .../platform_activity_observer_interface.dart | 1 + plugins/focus_window/lib/platform/windows.dart | 5 +++++ 10 files changed, 39 insertions(+), 18 deletions(-) diff --git a/lib/pages/settings/widgets/smart_paste_switch.dart b/lib/pages/settings/widgets/smart_paste_switch.dart index 1ffeba80..4e429e02 100644 --- a/lib/pages/settings/widgets/smart_paste_switch.dart +++ b/lib/pages/settings/widgets/smart_paste_switch.dart @@ -23,8 +23,13 @@ class SmartPasteSwitch extends StatelessWidget { builder: (context, state) { return SwitchListTile( value: state, - onChanged: (value) { - context.read().toggleSmartPaste(value); + onChanged: (value) async { + final cubit = context.read(); + final hasPermission = + await cubit.focusWindow.requestAccessibilityPermission(); + if (hasPermission) { + cubit.toggleSmartPaste(value); + } }, title: Text(context.locale.smartPaste), subtitle: Text( diff --git a/lib/widgets/window_focus_manager.dart b/lib/widgets/window_focus_manager.dart index a9cb7c25..67660c2e 100644 --- a/lib/widgets/window_focus_manager.dart +++ b/lib/widgets/window_focus_manager.dart @@ -118,24 +118,12 @@ class WindowFocusManagerState extends State print(data); } - void startListners() async { - // final hasGrant = - // await widget.focusWindow.isAccessibilityPermissionGranted(); - // print("GRANTED: $hasGrant"); - - // final activity = await widget.focusWindow.getActivity(); - // await widget.focusWindow.startObserver(); - // print("OBSERVING: ${widget.focusWindow.isObserving}"); - // subscription ??= widget.focusWindow.events.listen(onFocuswindowChange); - } - @override void initState() { super.initState(); windowManager.addListener(this); windowManager.setPreventClose(true); appConfigCubit = context.read(); - startListners(); } @override diff --git a/packages/copycat_base b/packages/copycat_base index acf81bd1..1bc2dba7 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit acf81bd1302792fb858e50da0b42eaf2c409e17a +Subproject commit 1bc2dba75ba69c81f7dcd1818130ace1b4ec7cf1 diff --git a/plugins/focus_window/lib/focus_window.dart b/plugins/focus_window/lib/focus_window.dart index abc56915..0e12802b 100644 --- a/plugins/focus_window/lib/focus_window.dart +++ b/plugins/focus_window/lib/focus_window.dart @@ -34,9 +34,14 @@ class FocusWindow { Future get isObserving => FocusWindowPlatform.instance.isObserving; Future requestAccessibilityPermission() { + // FocusWindowPlatform.instance. return FocusWindowPlatform.instance.requestAccessibilityPermission(); } + Future openAccessibilityPermissionSetting() { + return FocusWindowPlatform.instance.openAccessibilityPermissionSetting(); + } + Future startObserver() { return FocusWindowPlatform.instance.startObserver(); } diff --git a/plugins/focus_window/lib/focus_window_method_channel.dart b/plugins/focus_window/lib/focus_window_method_channel.dart index bbe6c336..017036ec 100644 --- a/plugins/focus_window/lib/focus_window_method_channel.dart +++ b/plugins/focus_window/lib/focus_window_method_channel.dart @@ -66,6 +66,11 @@ class MethodChannelFocusWindow extends FocusWindowPlatform { return activityObserver.requestAccessibilityPermission(); } + @override + Future openAccessibilityPermissionSetting() { + return activityObserver.openAccessibilityPermissionSetting(); + } + @override Future startObserver() { return activityObserver.startObserver(); diff --git a/plugins/focus_window/lib/focus_window_platform_interface.dart b/plugins/focus_window/lib/focus_window_platform_interface.dart index 9f87bf41..60c94544 100644 --- a/plugins/focus_window/lib/focus_window_platform_interface.dart +++ b/plugins/focus_window/lib/focus_window_platform_interface.dart @@ -60,6 +60,13 @@ abstract class FocusWindowPlatform extends PlatformInterface ); } + @override + Future openAccessibilityPermissionSetting() { + throw UnimplementedError( + "openAccessibilityPermissionSetting() has not been implemented", + ); + } + @override Future isAccessibilityPermissionGranted() { throw UnimplementedError( diff --git a/plugins/focus_window/lib/platform/macos.dart b/plugins/focus_window/lib/platform/macos.dart index f64cca0c..8541e10b 100644 --- a/plugins/focus_window/lib/platform/macos.dart +++ b/plugins/focus_window/lib/platform/macos.dart @@ -55,9 +55,9 @@ class MacosActivityObserver implements PlatformActivityObserverInterface { false; } - Future openAccessibilityPermissionSetting() async { - return await _channel.invokeMethod('openAccessibilityPermissionSetting') ?? - false; + @override + Future openAccessibilityPermissionSetting() async { + await _channel.invokeMethod('openAccessibilityPermissionSetting'); } @override diff --git a/plugins/focus_window/lib/platform/not_supported.dart b/plugins/focus_window/lib/platform/not_supported.dart index 5a9d7d5c..325e5db8 100644 --- a/plugins/focus_window/lib/platform/not_supported.dart +++ b/plugins/focus_window/lib/platform/not_supported.dart @@ -55,4 +55,9 @@ class NotSupportedPlatformActivityObserver Future setActiveWindowId(int windowId) { return Future.value(null); } + + @override + Future openAccessibilityPermissionSetting() async { + return; + } } diff --git a/plugins/focus_window/lib/platform/platform_activity_observer_interface.dart b/plugins/focus_window/lib/platform/platform_activity_observer_interface.dart index 4d612dc2..4df7e8ae 100644 --- a/plugins/focus_window/lib/platform/platform_activity_observer_interface.dart +++ b/plugins/focus_window/lib/platform/platform_activity_observer_interface.dart @@ -7,6 +7,7 @@ abstract class PlatformActivityObserverInterface { Future getActivity({bool withIcon = false}); Future requestAccessibilityPermission(); Future isAccessibilityPermissionGranted(); + Future openAccessibilityPermissionSetting(); Future startObserver(); Future stopObserver(); Future getActiveWindowId(); diff --git a/plugins/focus_window/lib/platform/windows.dart b/plugins/focus_window/lib/platform/windows.dart index f984c5cb..0ef4eda9 100644 --- a/plugins/focus_window/lib/platform/windows.dart +++ b/plugins/focus_window/lib/platform/windows.dart @@ -155,4 +155,9 @@ class WindowsActivityObserver implements PlatformActivityObserverInterface { Future pasteContent() async { simulateWindowsPasteShortcut(); } + + @override + Future openAccessibilityPermissionSetting() async { + return; + } } From b11acc73b93ccd8e94961cd9fcda5fa392a2410e Mon Sep 17 00:00:00 2001 From: Raj Date: Mon, 2 Sep 2024 00:31:42 +0530 Subject: [PATCH 06/15] add open button for video file types in preview dialog --- lib/pages/layout/navbar_layout.dart | 4 +- lib/pages/preview/widgets/media_preview.dart | 67 ++++++++++++------- lib/widgets/clip_cards/clip_card_body.dart | 4 +- .../clip_card_sync_status_footer.dart | 2 +- lib/widgets/event_bridge.dart | 38 +---------- lib/widgets/fabs/sync_status.dart | 5 -- lib/widgets/network_observer.dart | 11 +-- packages/copycat_base | 2 +- 8 files changed, 58 insertions(+), 75 deletions(-) diff --git a/lib/pages/layout/navbar_layout.dart b/lib/pages/layout/navbar_layout.dart index 22e8b3b1..4c2df797 100644 --- a/lib/pages/layout/navbar_layout.dart +++ b/lib/pages/layout/navbar_layout.dart @@ -1,4 +1,5 @@ import 'package:clipboard/utils/utility.dart'; +import 'package:clipboard/widgets/network_observer.dart'; import 'package:clipboard/widgets/titlebar.dart'; import 'package:copycat_base/common/logging.dart'; import 'package:flutter/material.dart'; @@ -24,8 +25,7 @@ class NavBarPage extends StatefulWidget { class _NavBarPageState extends State { @override Widget build(BuildContext context) { - //TODO::: Widget child = NetworkObserver(child: widget.child); - Widget child = widget.child; + Widget child = NetworkObserver(child: widget.child); if (isDesktopPlatform) child = TitlebarView(child: child); return PopScope( canPop: false, diff --git a/lib/pages/preview/widgets/media_preview.dart b/lib/pages/preview/widgets/media_preview.dart index 9d546d72..ad0a9b09 100644 --- a/lib/pages/preview/widgets/media_preview.dart +++ b/lib/pages/preview/widgets/media_preview.dart @@ -1,9 +1,11 @@ import 'dart:typed_data'; import 'package:blurhash_dart/blurhash_dart.dart'; +import 'package:clipboard/utils/clipboard_actions.dart'; import 'package:copycat_base/constants/strings/asset_constants.dart'; import 'package:copycat_base/constants/widget_styles.dart'; import 'package:copycat_base/db/clipboard_item/clipboard_item.dart'; +import 'package:copycat_base/l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg_provider/flutter_svg_provider.dart'; import 'package:image/image.dart' as img; @@ -18,7 +20,7 @@ class MediaClipPreviewCard extends StatelessWidget { required this.isMobile, }); - ImageProvider getPreview() { + ImageProvider? getPreview() { if (item.localPath != null) { if (item.fileMimeType!.contains("svg")) { return Svg( @@ -40,35 +42,55 @@ class MediaClipPreviewCard extends StatelessWidget { } } - Icon getIcon() { + void open() async { + openFile(item); + } + + Widget? getPrimaryView(BuildContext context) { if (item.fileMimeType != null) { if (item.fileMimeType!.startsWith("image")) { - return const Icon( - Icons.image, - color: Colors.white, + return const Align( + alignment: Alignment(-.98, -.98), + child: Icon( + Icons.image, + color: Colors.white, + ), ); } if (item.fileMimeType!.startsWith("video")) { - return const Icon( - Icons.ondemand_video_rounded, - color: Colors.white, + if (item.inCache) { + return Center( + child: ElevatedButton.icon( + icon: const Icon(Icons.play_arrow_rounded), + onPressed: open, + label: Text(context.locale.open), + ), + ); + } + return const Align( + alignment: Alignment(-.98, -.98), + child: Icon( + Icons.video_file, + color: Colors.white, + ), ); } if (item.fileMimeType!.startsWith("audio")) { - return const Icon( - Icons.audiotrack, - color: Colors.white, + return const Align( + alignment: Alignment(-.98, -.98), + child: Icon( + Icons.audiotrack, + color: Colors.white, + ), ); } } - return const Icon( - Icons.image, - color: Colors.white, - ); + return null; } @override Widget build(BuildContext context) { + final preview = getPreview(); return Card.filled( margin: isMobile ? const EdgeInsets.only( @@ -86,20 +108,19 @@ class MediaClipPreviewCard extends StatelessWidget { ), child: DecoratedBox( decoration: BoxDecoration( - image: DecorationImage( - image: getPreview(), - fit: BoxFit.contain, - ), + image: preview != null + ? DecorationImage( + image: preview, + fit: BoxFit.contain, + ) + : null, borderRadius: isMobile ? radius12 : const BorderRadius.horizontal( left: Radius.circular(12), ), ), - child: Align( - alignment: const Alignment(0.95, 0.95), - child: getIcon(), - ), + child: getPrimaryView(context), ), ); } diff --git a/lib/widgets/clip_cards/clip_card_body.dart b/lib/widgets/clip_cards/clip_card_body.dart index c113f24c..888713d2 100644 --- a/lib/widgets/clip_cards/clip_card_body.dart +++ b/lib/widgets/clip_cards/clip_card_body.dart @@ -63,14 +63,14 @@ class ClipCardBodyContent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - if (item.title != null) + if (item.displayTitle != null) Padding( padding: const EdgeInsets.symmetric( horizontal: padding8, vertical: padding2, ), child: Text( - item.title!, + item.displayTitle!, style: textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/clip_cards/clip_card_sync_status_footer.dart b/lib/widgets/clip_cards/clip_card_sync_status_footer.dart index b8faa565..b273e3e7 100644 --- a/lib/widgets/clip_cards/clip_card_sync_status_footer.dart +++ b/lib/widgets/clip_cards/clip_card_sync_status_footer.dart @@ -32,7 +32,7 @@ class ClipCardSyncStatusFooter extends StatelessWidget { buttonText = context.locale.uploading; } } else { - buttonText = context.locale.syncing(''); + buttonText = context.locale.syncing; } } else { buttonText = context.locale.sync; diff --git a/lib/widgets/event_bridge.dart b/lib/widgets/event_bridge.dart index 534dff6f..f307e039 100644 --- a/lib/widgets/event_bridge.dart +++ b/lib/widgets/event_bridge.dart @@ -1,6 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'package:clipboard/routes/utils.dart'; import 'package:clipboard/widgets/dialogs/inconsistent_timing.dart'; import 'package:copycat_base/bloc/app_config_cubit/app_config_cubit.dart'; import 'package:copycat_base/bloc/auth_cubit/auth_cubit.dart'; @@ -109,41 +108,8 @@ class EventBridge extends StatelessWidget { case CloudPersistanceDeleted(:final item): context.read().delete(item); break; - case CloudPersistanceError( - :final retryCount, - :final failedAction, - :final item - ): - { - if (retryCount.isNegative) return; - if (retryCount > 3) return; - - await waitHere(retryCount + 1); - - switch (failedAction) { - case FailedAction.create || - FailedAction.update || - FailedAction.upload: - context.read().persist( - item, - retryCount: retryCount + 1, - ); - break; - case FailedAction.download: - context.read().download( - item, - retryCount: retryCount + 1, - ); - break; - case FailedAction.delete: - context.read().delete( - item, - retryCount: retryCount + 1, - ); - break; - default: - } - } + case CloudPersistanceError(:final failure, :final item): + {} break; case _: } diff --git a/lib/widgets/fabs/sync_status.dart b/lib/widgets/fabs/sync_status.dart index 729cd8b6..4500b730 100644 --- a/lib/widgets/fabs/sync_status.dart +++ b/lib/widgets/fabs/sync_status.dart @@ -33,11 +33,6 @@ class SyncStatusFAB extends StatelessWidget { icon = Icons.sync_lock_rounded; message = context.locale.syncNotAvailable; break; - case SyncingState(:final progress, :final total): - disabled = true; - isSyncing = true; - message = context.locale.syncing("$progress/$total"); - break; case SyncCheckFailedState(:final failure): disabled = false; isSyncing = false; diff --git a/lib/widgets/network_observer.dart b/lib/widgets/network_observer.dart index 576da23a..125be312 100644 --- a/lib/widgets/network_observer.dart +++ b/lib/widgets/network_observer.dart @@ -22,14 +22,14 @@ class NetworkObserver extends StatefulWidget { } class _NetworkObserverState extends State { - late StreamSubscription subscription; + StreamSubscription? subscription; bool wasDisconnected = false; late AuthCubit authCubit; late MonetizationCubit monetizationCubit; late DriveSetupCubit driveSetupCubit; late AppConfigCubit appConfigCubit; - late final Stream networkObserver; + Stream? networkObserver; bool transformNetworkStatus(InternetStatus event) { return event == InternetStatus.connected; @@ -37,17 +37,18 @@ class _NetworkObserverState extends State { @override void dispose() { - subscription.cancel(); + subscription?.cancel(); super.dispose(); } @override void initState() { super.initState(); + authCubit = BlocProvider.of(context); + if (authCubit.isLocalAuth) return; networkObserver = InternetConnection().onStatusChange.map(transformNetworkStatus); - subscription = networkObserver.listen(onConnectionChanged); - authCubit = BlocProvider.of(context); + subscription = networkObserver?.listen(onConnectionChanged); monetizationCubit = BlocProvider.of(context); driveSetupCubit = BlocProvider.of(context); appConfigCubit = BlocProvider.of(context); diff --git a/packages/copycat_base b/packages/copycat_base index 1bc2dba7..38e977fc 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 1bc2dba75ba69c81f7dcd1818130ace1b4ec7cf1 +Subproject commit 38e977fcdf45f2331ddc2d4c29ed88570bbcb477 From 9b5ded18ea3b5c2c9d96cea47a283d0636df7381 Mon Sep 17 00:00:00 2001 From: Raj Date: Mon, 2 Sep 2024 01:38:09 +0530 Subject: [PATCH 07/15] fix animation on some pages and dialogs --- .../collections/pages/create_edit/page.dart | 7 +-- .../widgets/collection_list_item.dart | 50 +++++++++++-------- .../widgets/dialogs/create_collection.dart | 2 +- lib/pages/preview/page.dart | 12 +++-- lib/routes/routes.dart | 47 ++++++++--------- lib/widgets/dialogs/collection_selector.dart | 10 ++-- .../page_route/dynamic_page_route.dart | 35 ++++++------- 7 files changed, 90 insertions(+), 73 deletions(-) diff --git a/lib/pages/collections/pages/create_edit/page.dart b/lib/pages/collections/pages/create_edit/page.dart index 02c727c9..d622b6b1 100644 --- a/lib/pages/collections/pages/create_edit/page.dart +++ b/lib/pages/collections/pages/create_edit/page.dart @@ -1,4 +1,5 @@ import 'package:clipboard/pages/collections/pages/create_edit/widgets/create_edit_form.dart'; +import 'package:copycat_base/constants/numbers/breakpoints.dart'; import 'package:copycat_base/constants/widget_styles.dart'; import 'package:copycat_base/db/clip_collection/clipcollection.dart'; import 'package:copycat_base/l10n/l10n.dart'; @@ -55,13 +56,11 @@ class ClipCollectionCreateEditMobilePageContent extends StatelessWidget { } class ClipCollectionCreateEditPage extends StatelessWidget { - final bool isDialog; final ClipCollection? collection; const ClipCollectionCreateEditPage({ super.key, this.collection, - this.isDialog = false, }); @override @@ -69,7 +68,9 @@ class ClipCollectionCreateEditPage extends StatelessWidget { final title = collection == null ? context.locale.createCollection : context.locale.editCollection; - if (isDialog) { + final width = MediaQuery.of(context).size.width; + final smallScreen = Breakpoints.isMobile(width); + if (!smallScreen) { return ClipCollectionCreateEditDesktopPageContent( title: title, collection: collection, diff --git a/lib/pages/collections/widgets/collection_list_item.dart b/lib/pages/collections/widgets/collection_list_item.dart index 9a662fbb..54d813f1 100644 --- a/lib/pages/collections/widgets/collection_list_item.dart +++ b/lib/pages/collections/widgets/collection_list_item.dart @@ -59,27 +59,35 @@ class ClipCollectionListItem extends StatelessWidget { onPressed: () => delete(context), ), ], - child: ListTile( - shape: shape, - autofocus: autoFocus, - leading: Text( - collection.emoji, - style: textTheme.headlineMedium, - ), - title: Text(collection.title, maxLines: 1), - titleTextStyle: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - subtitle: Text( - collection.description ?? context.locale.noDescription, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - isThreeLine: true, - titleAlignment: ListTileTitleAlignment.center, - trailing: const Icon(Icons.chevron_right), - onTap: () => showDetail(context), - ), + child: Builder(builder: (context) { + return GestureDetector( + onSecondaryTapDown: (detail) { + final position = detail.globalPosition; + Menu.of(context).openPopupMenu(context, position); + }, + child: ListTile( + shape: shape, + autofocus: autoFocus, + leading: Text( + collection.emoji, + style: textTheme.headlineMedium, + ), + title: Text(collection.title, maxLines: 1), + titleTextStyle: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + subtitle: Text( + collection.description ?? context.locale.noDescription, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + isThreeLine: true, + titleAlignment: ListTileTitleAlignment.center, + trailing: const Icon(Icons.chevron_right), + onTap: () => showDetail(context), + ), + ); + }), ); } } diff --git a/lib/pages/collections/widgets/dialogs/create_collection.dart b/lib/pages/collections/widgets/dialogs/create_collection.dart index e519c0bb..04e3f235 100644 --- a/lib/pages/collections/widgets/dialogs/create_collection.dart +++ b/lib/pages/collections/widgets/dialogs/create_collection.dart @@ -20,7 +20,7 @@ Future showCreateCollectionDialog(BuildContext context) async { showDialog( context: context, builder: (context) => const Dialog( - child: ClipCollectionCreateEditPage(isDialog: true), + child: ClipCollectionCreateEditPage(), ), ); } diff --git a/lib/pages/preview/page.dart b/lib/pages/preview/page.dart index 3c128691..dbd7b46d 100644 --- a/lib/pages/preview/page.dart +++ b/lib/pages/preview/page.dart @@ -4,6 +4,7 @@ import 'package:clipboard/pages/preview/widgets/media_preview.dart'; import 'package:clipboard/pages/preview/widgets/preview_options.dart'; import 'package:clipboard/pages/preview/widgets/text_preview.dart'; import 'package:clipboard/pages/preview/widgets/url_preview.dart'; +import 'package:copycat_base/constants/numbers/breakpoints.dart'; import 'package:copycat_base/constants/widget_styles.dart'; import 'package:copycat_base/db/clipboard_item/clipboard_item.dart'; import 'package:copycat_base/enums/clip_type.dart'; @@ -123,22 +124,27 @@ class ClipboardItemMobilePreviewPageContent extends StatelessWidget { } class ClipboardItemPreviewPage extends StatelessWidget { - final bool isDialog; final ClipboardItem item; const ClipboardItemPreviewPage({ super.key, required this.item, - this.isDialog = false, }); @override Widget build(BuildContext context) { - if (isDialog) { + final width = MediaQuery.of(context).size.width; + final smallScreen = Breakpoints.isMobile(width); + if (!smallScreen) { return ClipboardItemDesktopPreviewPageContent(item: item); } return Scaffold( appBar: AppBar( title: Text(context.locale.preview), + actions: const [ + CloseButton(), + width12, + ], + automaticallyImplyLeading: false, ), body: ClipboardItemMobilePreviewPageContent(item: item), ); diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 10041891..d688973c 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -94,7 +94,8 @@ GoRouter router([List? observers]) => GoRouter( context.read().getItem(id: id); return DynamicPage( key: state.pageKey, - builder: (context, isDialog) => FutureBuilder( + fullScreenDialog: true, + child: FutureBuilder( future: item, builder: (BuildContext context, AsyncSnapshot snapshot) { if (!snapshot.hasData) { @@ -104,7 +105,6 @@ GoRouter router([List? observers]) => GoRouter( } return ClipboardItemPreviewPage( item: snapshot.data, - isDialog: isDialog, ); }, ), @@ -214,29 +214,30 @@ GoRouter router([List? observers]) => GoRouter( return DynamicPage( key: state.pageKey, - builder: (context, isDialog) { - if (id == "new") { - return ClipCollectionCreateEditPage( - isDialog: isDialog); - } - final id_ = int.parse(id); + child: Builder( + builder: (context) { + if (id == "new") { + return const ClipCollectionCreateEditPage(); + } + final id_ = int.parse(id); - return FutureBuilder( - future: context.read().get(id_), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), + return FutureBuilder( + future: + context.read().get(id_), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ClipCollectionCreateEditPage( + collection: snapshot.data, ); - } - return ClipCollectionCreateEditPage( - isDialog: isDialog, - collection: snapshot.data, - ); - }, - ); - }, + }, + ); + }, + ), ); }, ), diff --git a/lib/widgets/dialogs/collection_selector.dart b/lib/widgets/dialogs/collection_selector.dart index 7113008d..f5d09c3b 100644 --- a/lib/widgets/dialogs/collection_selector.dart +++ b/lib/widgets/dialogs/collection_selector.dart @@ -2,6 +2,7 @@ import 'package:clipboard/widgets/fabs/create_collection.dart'; import 'package:clipboard/widgets/local_user.dart'; import 'package:clipboard/widgets/no_collection.dart'; import 'package:copycat_base/bloc/clip_collection_cubit/clip_collection_cubit.dart'; +import 'package:copycat_base/constants/widget_styles.dart'; import 'package:copycat_base/db/clip_collection/clipcollection.dart'; import 'package:copycat_base/l10n/l10n.dart'; import 'package:copycat_base/utils/common_extension.dart'; @@ -19,11 +20,13 @@ class ClipCollectionSelectionDialog extends StatelessWidget { Future open(BuildContext context) async { return await showDialog( context: context, - builder: (context) => SimpleDialog( + builder: (context) => AlertDialog( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(context.locale.selectCollection), + FittedBox( + child: Text(context.locale.selectCollection), + ), const DisableForLocalUser( ifLocal: CreateCollectionFAB( localMode: true, @@ -33,7 +36,8 @@ class ClipCollectionSelectionDialog extends StatelessWidget { ), ], ), - children: [this], + content: this, + insetPadding: const EdgeInsets.all(padding12), ), ); } diff --git a/lib/widgets/page_route/dynamic_page_route.dart b/lib/widgets/page_route/dynamic_page_route.dart index c1b827a0..9b9605ad 100644 --- a/lib/widgets/page_route/dynamic_page_route.dart +++ b/lib/widgets/page_route/dynamic_page_route.dart @@ -1,25 +1,16 @@ import 'package:copycat_base/constants/numbers/breakpoints.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; -typedef DynamicWidgetBuilder = Widget Function( - BuildContext context, bool isDialog); - -class DynamicPage extends Page { +class DynamicPage extends CustomTransitionPage { final Offset? anchorPoint; - final Color? barrierColor; - final bool barrierDismissible; - final String? barrierLabel; final bool useSafeArea; final CapturedThemes? themes; - final DynamicWidgetBuilder builder; final bool fullScreenDialog; - const DynamicPage({ - required this.builder, + DynamicPage({ + required super.child, this.anchorPoint, - this.barrierColor = Colors.black87, - this.barrierDismissible = true, - this.barrierLabel, this.useSafeArea = true, this.fullScreenDialog = false, this.themes, @@ -27,7 +18,15 @@ class DynamicPage extends Page { super.name, super.arguments, super.restorationId, - }); + }) : super( + transitionsBuilder: (context, animation, secondaryAnimation, child) => + ScaleTransition( + scale: animation, + child: child, + ), + barrierColor: Colors.black54, + barrierDismissible: true, + ); @override Route createRoute(BuildContext context) { @@ -37,8 +36,8 @@ class DynamicPage extends Page { if (Breakpoints.isMobile(width)) { return MaterialPageRoute( settings: this, - builder: (context) => builder(context, false), - fullscreenDialog: fullScreenDialog, + builder: (context) => child, + fullscreenDialog: false, maintainState: true, barrierDismissible: barrierDismissible, ); @@ -47,9 +46,7 @@ class DynamicPage extends Page { return DialogRoute( context: context, settings: this, - builder: (context) => Dialog( - child: builder(context, true), - ), + builder: (context) => Dialog(child: child), anchorPoint: anchorPoint, barrierColor: barrierColor, barrierDismissible: barrierDismissible, From 74b3cc0a1bcd56d9dbf1adcfc0eb67d235098e8e Mon Sep 17 00:00:00 2001 From: Raj Date: Mon, 2 Sep 2024 10:14:02 +0530 Subject: [PATCH 08/15] fix: #7 --- lib/widgets/event_bridge.dart | 4 +++- packages/copycat_base | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/widgets/event_bridge.dart b/lib/widgets/event_bridge.dart index f307e039..d8bd3a29 100644 --- a/lib/widgets/event_bridge.dart +++ b/lib/widgets/event_bridge.dart @@ -109,7 +109,9 @@ class EventBridge extends StatelessWidget { context.read().delete(item); break; case CloudPersistanceError(:final failure, :final item): - {} + { + // TODO: improve retry strategy + } break; case _: } diff --git a/packages/copycat_base b/packages/copycat_base index 38e977fc..1c5904b8 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 38e977fcdf45f2331ddc2d4c29ed88570bbcb477 +Subproject commit 1c5904b84bcb0bc11263bf493ddb32e9cdb426fa From a531cb54488844b7541f78298647208e4494af4a Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 17:08:33 +0530 Subject: [PATCH 09/15] fix: #9 --- lib/pages/preview/widgets/clip_detail_form.dart | 5 +++-- packages/copycat_base | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pages/preview/widgets/clip_detail_form.dart b/lib/pages/preview/widgets/clip_detail_form.dart index e89ab70b..e93b4944 100644 --- a/lib/pages/preview/widgets/clip_detail_form.dart +++ b/lib/pages/preview/widgets/clip_detail_form.dart @@ -5,6 +5,7 @@ import 'package:copycat_base/db/clip_collection/clipcollection.dart'; import 'package:copycat_base/db/clipboard_item/clipboard_item.dart'; import 'package:copycat_base/l10n/l10n.dart'; import 'package:copycat_base/utils/common_extension.dart'; +import 'package:copycat_base/utils/utility.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:form_validator/form_validator.dart'; @@ -79,8 +80,8 @@ class _ClipDetailFormState extends State { : descriptionController.text.trim(); final updatedItem = widget.item.copyWith( - title: title, - description: description, + title: cleanUpString(title), + description: cleanUpString(description), collectionId: collectionId?.$1, serverCollectionId: collectionId?.$2, )..applyId(widget.item); diff --git a/packages/copycat_base b/packages/copycat_base index 1c5904b8..df6be3d0 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 1c5904b84bcb0bc11263bf493ddb32e9cdb426fa +Subproject commit df6be3d058b21509b89ebf262358a7c63c53b1f4 From d37d5119ed8359d0024859d982cced03448bc526 Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 18:13:26 +0530 Subject: [PATCH 10/15] reduces frequent network requests --- lib/main.dart | 1 + .../keyboard_shortcut_provider.dart | 8 +------ lib/widgets/event_bridge.dart | 22 +++++++++++++++++-- packages/copycat_base | 2 +- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index dcd7af58..6aec27dd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -137,6 +137,7 @@ class AppContent extends StatelessWidget { syncCubit.syncHours = subscription.syncHours; syncCubit.syncChanges(force: true); context.read().load(subscription); + context.read().loadSub(subscription); } } }, diff --git a/lib/routes/keyboard_shortcuts/keyboard_shortcut_provider.dart b/lib/routes/keyboard_shortcuts/keyboard_shortcut_provider.dart index b8764ead..6f8a1623 100644 --- a/lib/routes/keyboard_shortcuts/keyboard_shortcut_provider.dart +++ b/lib/routes/keyboard_shortcuts/keyboard_shortcut_provider.dart @@ -6,10 +6,8 @@ import 'package:clipboard/widgets/window_focus_manager.dart'; import 'package:copycat_base/bloc/sync_manager_cubit/sync_manager_cubit.dart'; import 'package:copycat_base/common/events.dart'; import 'package:copycat_base/constants/strings/route_constants.dart'; -import 'package:copycat_base/utils/snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:universal_io/io.dart'; @@ -167,12 +165,8 @@ class KeyboardShortcutProvider extends StatelessWidget { ), RefreshIntent: CallbackAction( onInvoke: (intent) async { - final cubit = context.read(); - final failure = await cubit.syncChanges(force: true); + await syncChanges(context); - if (failure != null) { - showFailureSnackbar(failure); - } return null; }, ), diff --git a/lib/widgets/event_bridge.dart b/lib/widgets/event_bridge.dart index d8bd3a29..c824e08d 100644 --- a/lib/widgets/event_bridge.dart +++ b/lib/widgets/event_bridge.dart @@ -8,6 +8,7 @@ import 'package:copycat_base/bloc/cloud_persistance_cubit/cloud_persistance_cubi import 'package:copycat_base/bloc/offline_persistance_cubit/offline_persistance_cubit.dart'; import 'package:copycat_base/bloc/sync_manager_cubit/sync_manager_cubit.dart'; import 'package:copycat_base/data/services/encryption.dart'; +import 'package:copycat_base/db/clipboard_item/clipboard_item.dart'; import 'package:copycat_base/utils/snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,6 +21,16 @@ class EventBridge extends StatelessWidget { required this.child, }); + bool shouldSync(List? updatedFields, ClipboardItem item) { + if (updatedFields == null) return true; + if (updatedFields.contains("copiedCount") && item.copiedCount % 10 == 0) { + // if only copied count is changed then only sync after every 10 copy operations. + return true; + } + + return false; + } + @override Widget build(BuildContext context) { return MultiBlocListener( @@ -87,8 +98,14 @@ class EventBridge extends StatelessWidget { BlocListener( listener: (context, state) async { switch (state) { - case OfflinePersistanceSaved(:final item, synced: false): - context.read().persist(item); + case OfflinePersistanceSaved( + :final item, + synced: false, + :final updatedFields + ): + if (shouldSync(updatedFields, item)) { + context.read().persist(item); + } break; case OfflinePersistanceError(:final failure): showFailureSnackbar(failure); @@ -111,6 +128,7 @@ class EventBridge extends StatelessWidget { case CloudPersistanceError(:final failure, :final item): { // TODO: improve retry strategy + showFailureSnackbar(failure); } break; case _: diff --git a/packages/copycat_base b/packages/copycat_base index df6be3d0..8228b9b1 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit df6be3d058b21509b89ebf262358a7c63c53b1f4 +Subproject commit 8228b9b1b05fa326e29219b9d4143f2609adbcda From c4ce8d161bcaa5745cce81dc3702397f7d40930f Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 18:17:12 +0530 Subject: [PATCH 11/15] _ --- packages/copycat_base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/copycat_base b/packages/copycat_base index 8228b9b1..52a1a7f9 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 8228b9b1b05fa326e29219b9d4143f2609adbcda +Subproject commit 52a1a7f9c49aa21ad239e665bc48c1f3e5dce434 From 320f648eec7c2d7e204026273a1cae07c7415ab0 Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 18:18:14 +0530 Subject: [PATCH 12/15] _ --- lib/widgets/event_bridge.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/event_bridge.dart b/lib/widgets/event_bridge.dart index c824e08d..6f50fb41 100644 --- a/lib/widgets/event_bridge.dart +++ b/lib/widgets/event_bridge.dart @@ -125,7 +125,7 @@ class EventBridge extends StatelessWidget { case CloudPersistanceDeleted(:final item): context.read().delete(item); break; - case CloudPersistanceError(:final failure, :final item): + case CloudPersistanceError(:final failure): { // TODO: improve retry strategy showFailureSnackbar(failure); diff --git a/pubspec.yaml b/pubspec.yaml index 0093cd6b..f4ddc570 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: clipboard description: "CopyCat Clipboard: One Clipboard, Limitless Productivity" publish_to: 'none' -version: 0.1.22+52 +version: 0.1.23+53 environment: sdk: '>=3.3.3 <4.0.0' From 77de93d05d43a8c1f564e2703ee7b83f1859db2a Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 18:45:22 +0530 Subject: [PATCH 13/15] _ --- packages/copycat_pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/copycat_pro b/packages/copycat_pro index 37b49dfe..7659d5ae 160000 --- a/packages/copycat_pro +++ b/packages/copycat_pro @@ -1 +1 @@ -Subproject commit 37b49dfe7af4d9daf7406e8758d80f71e3630888 +Subproject commit 7659d5ae72f0974f3134b3df3726bbeb681e4a61 From 1d45c7fd2771b5726559ff925c086f289e745154 Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 18:49:27 +0530 Subject: [PATCH 14/15] fix: collection setting not visible --- lib/pages/collections/widgets/collection_list_item.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/collections/widgets/collection_list_item.dart b/lib/pages/collections/widgets/collection_list_item.dart index 54d813f1..379060f8 100644 --- a/lib/pages/collections/widgets/collection_list_item.dart +++ b/lib/pages/collections/widgets/collection_list_item.dart @@ -84,6 +84,7 @@ class ClipCollectionListItem extends StatelessWidget { isThreeLine: true, titleAlignment: ListTileTitleAlignment.center, trailing: const Icon(Icons.chevron_right), + onLongPress: () => Menu.of(context).openOptionDialog(context), onTap: () => showDetail(context), ), ); From 7209be6d56d6600923efec10a689b276f031dc76 Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 3 Sep 2024 19:54:02 +0530 Subject: [PATCH 15/15] windows build --- .gitmodules | 4 ++-- packages/copycat_base | 2 +- pubspec.lock | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.gitmodules b/.gitmodules index ea290a68..93e6400c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ [submodule "packages/copycat_pro"] path = packages/copycat_pro - url = git@github.com:raj457036/copycat_pro.git + url = ssh://git@github.com:raj457036/copycat_pro.git update = merge [submodule "packages/copycat_base"] path = packages/copycat_base - url = git@github.com:raj457036/copycat_base.git + url = ssh://git@github.com:raj457036/copycat_base.git update = merge diff --git a/packages/copycat_base b/packages/copycat_base index 52a1a7f9..422047ee 160000 --- a/packages/copycat_base +++ b/packages/copycat_base @@ -1 +1 @@ -Subproject commit 52a1a7f9c49aa21ad239e665bc48c1f3e5dce434 +Subproject commit 422047ee6e9945b29437008c70ff6fc140341f47 diff --git a/pubspec.lock b/pubspec.lock index 9b587e35..128c8fbd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1047,18 +1047,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -1103,10 +1103,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" menu_base: dependency: transitive description: @@ -1119,10 +1119,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: "direct main" description: @@ -1734,10 +1734,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" time: dependency: transitive description: @@ -1926,10 +1926,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: