├── linux ├── .gitignore ├── main.cc ├── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ ├── generated_plugin_registrant.cc │ └── CMakeLists.txt ├── my_application.h ├── my_application.cc └── CMakeLists.txt ├── scripts ├── platforms │ ├── windows │ │ ├── package.bat │ │ └── build.bat │ ├── macos │ │ └── build.sh │ └── linux │ │ └── build.sh ├── common │ ├── config.json │ ├── utils.sh │ └── logger.sh ├── build.sh └── build.bat ├── .gitattributes ├── README.assets ├── IMG_0060.jpg ├── 20250106143024.jpg ├── 20250106143037.jpg ├── 20250106143713.jpg ├── 20250107164712.jpg └── 20250107184507.jpg ├── macos ├── Runner │ ├── app_icon_1024.png │ ├── Configs │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Warnings.xcconfig │ │ └── AppInfo.xcconfig │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── icon.png │ │ │ └── Contents.json │ ├── Release.entitlements │ ├── DebugProfile.entitlements │ ├── com.example.easyPasta.plist │ ├── AppDelegate.swift │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── WindowUtils.swift ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── RunnerTests │ └── RunnerTests.swift ├── Podfile └── Podfile.lock ├── assets └── images │ ├── tray_icon_original.ico │ ├── tray_icon_original.png │ └── tray_icon_original.icns ├── windows ├── runner │ ├── resources │ │ └── app_icon.ico │ ├── resource.h │ ├── utils.h │ ├── runner.exe.manifest │ ├── flutter_window.h │ ├── main.cpp │ ├── CMakeLists.txt │ ├── utils.cpp │ ├── flutter_window.cpp │ ├── Runner.rc │ └── win32_window.h ├── .gitignore ├── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ ├── generated_plugin_registrant.cc │ └── CMakeLists.txt └── CMakeLists.txt ├── devtools_options.yaml ├── .idea ├── vcs.xml └── workspace.xml ├── lib ├── core │ ├── tray_service.dart │ ├── icon_service.dart │ ├── html_processor.dart │ ├── window_service.dart │ ├── dynamic_content_hash.dart │ ├── startup_service.dart │ ├── hotkey_service.dart │ ├── settings_service.dart │ ├── record_hotkey_dialog.dart │ ├── super_clipboard.dart │ └── bonsoir_service.dart ├── model │ ├── setting_item.dart │ ├── pboard_sort_type.dart │ ├── settings_model.dart │ ├── clipboard_type.dart │ ├── app_theme.dart │ ├── settings_constants.dart │ └── pasteboard_model.dart ├── page │ ├── empty_view.dart │ ├── confirm_dialog_view.dart │ ├── pboard_card_view.dart │ ├── home_page_view.dart │ ├── grid_view.dart │ ├── app_bar_view.dart │ └── settings_page.dart ├── widget │ ├── cards │ │ ├── image_card.dart │ │ ├── source_card.dart │ │ ├── html_card.dart │ │ ├── app_info_card.dart │ │ ├── text_card.dart │ │ ├── footer_card.dart │ │ └── file_card.dart │ ├── search_text_field.dart │ ├── setting_tiles.dart │ ├── preview_dialog.dart │ └── setting_counter.dart ├── main.dart ├── providers │ └── theme_provider.dart └── db │ └── shared_preference_helper.dart ├── .vscode └── launch.json ├── .gitignore ├── pubspec.yaml ├── LICENSE ├── test └── widget_test.dart ├── analysis_options.yaml ├── .metadata ├── .cursorrules └── README.md /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /scripts/platforms/windows/package.bat: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.assets/IMG_0060.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/README.assets/IMG_0060.jpg -------------------------------------------------------------------------------- /macos/Runner/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/macos/Runner/app_icon_1024.png -------------------------------------------------------------------------------- /README.assets/20250106143024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/README.assets/20250106143024.jpg -------------------------------------------------------------------------------- /README.assets/20250106143037.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/README.assets/20250106143037.jpg -------------------------------------------------------------------------------- /README.assets/20250106143713.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/README.assets/20250106143713.jpg -------------------------------------------------------------------------------- /README.assets/20250107164712.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/README.assets/20250107164712.jpg -------------------------------------------------------------------------------- /README.assets/20250107184507.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/README.assets/20250107184507.jpg -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /assets/images/tray_icon_original.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/assets/images/tray_icon_original.ico -------------------------------------------------------------------------------- /assets/images/tray_icon_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/assets/images/tray_icon_original.png -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /assets/images/tray_icon_original.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/assets/images/tray_icon_original.icns -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DargonLee/easy_pasta/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | - provider: true -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/Runner/com.example.easyPasta.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.example.easyPasta 7 | ProgramArguments 8 | 9 | /Applications/EasyPasta.app/Contents/MacOS/EasyPasta 10 | 11 | RunAtLoad 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | // MARK: - Properties 7 | // private let statusBarManager = StatusBarManager.shared 8 | 9 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 10 | return false 11 | } 12 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 13 | return true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "4ffd020922d5cb1e4bad73064b0d31b058749aa71ce2bf650dc9871b0d3d582e", 3 | "pins" : [ 4 | { 5 | "identity" : "launchatlogin", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/LaunchAtLogin", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "9a894d799269cb591037f9f9cb0961510d4dca81" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "4ffd020922d5cb1e4bad73064b0d31b058749aa71ce2bf650dc9871b0d3d582e", 3 | "pins" : [ 4 | { 5 | "identity" : "launchatlogin", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/LaunchAtLogin", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "9a894d799269cb591037f9f9cb0961510d4dca81" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /lib/core/tray_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform; 2 | import 'package:tray_manager/tray_manager.dart'; 3 | 4 | class TrayService { 5 | static final TrayService _instance = TrayService._internal(); 6 | factory TrayService() => _instance; 7 | TrayService._internal() { 8 | init(); 9 | } 10 | 11 | Future init() async { 12 | await trayManager.setIcon( 13 | Platform.isWindows 14 | ? 'assets/images/tray_icon_original.ico' 15 | : 'assets/images/tray_icon_original.png', 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/model/setting_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | enum SettingType { hotkey, theme, autoLaunch, maxStorage, clearData, exitApp, about } 4 | 5 | class SettingItem { 6 | final SettingType type; 7 | final String title; 8 | final String subtitle; 9 | final IconData icon; 10 | final Color? iconColor; 11 | final Color? textColor; 12 | 13 | const SettingItem({ 14 | required this.type, 15 | required this.title, 16 | required this.subtitle, 17 | required this.icon, 18 | this.iconColor, 19 | this.textColor, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lib/model/pboard_sort_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum NSPboardSortType { all, text, image, file, favorite } 4 | 5 | typedef FilterOption = ({String label, IconData icon, NSPboardSortType type}); 6 | final filterOptions = [ 7 | (label: '全部', icon: Icons.all_inclusive, type: NSPboardSortType.all), 8 | (label: '文本', icon: Icons.text_fields, type: NSPboardSortType.text), 9 | (label: '图片', icon: Icons.image, type: NSPboardSortType.image), 10 | (label: '文件', icon: Icons.folder, type: NSPboardSortType.file), 11 | (label: '收藏', icon: Icons.favorite, type: NSPboardSortType.favorite), 12 | ]; -------------------------------------------------------------------------------- /lib/model/settings_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum SettingType { 4 | hotkey, 5 | theme, 6 | autoLaunch, 7 | maxStorage, 8 | clearData, 9 | exitApp, 10 | about, 11 | bonjour, 12 | } 13 | 14 | class SettingItem { 15 | final SettingType type; 16 | final String title; 17 | final String subtitle; 18 | final IconData icon; 19 | final Color? iconColor; 20 | final Color? textColor; 21 | 22 | const SettingItem({ 23 | required this.type, 24 | required this.title, 25 | required this.subtitle, 26 | required this.icon, 27 | this.iconColor, 28 | this.textColor, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = EasyPasta 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.easyPasta 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "easy_pasta", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "easy_pasta (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "easy_pasta (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /lib/page/empty_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyStateView extends StatelessWidget { 4 | const EmptyStateView({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: Column( 10 | mainAxisAlignment: MainAxisAlignment.center, 11 | children: [ 12 | Icon(Icons.content_paste, size: 64, color: Colors.grey[400]), 13 | const SizedBox(height: 16), 14 | Text( 15 | '暂无数据', 16 | style: TextStyle( 17 | fontSize: 20, 18 | color: Colors.grey[600], 19 | fontWeight: FontWeight.w500, 20 | ), 21 | ), 22 | ], 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # dotenv environment variables file 15 | .env* 16 | 17 | # Avoid committing generated Javascript files: 18 | *.dart.js 19 | *.info.json # Produced by the --dump-info flag. 20 | *.js # When generated by dart2js. Don't specify *.js if your 21 | # project includes source files written in JavaScript. 22 | *.js_ 23 | *.js.deps 24 | *.js.map 25 | 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /scripts/common/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "EasyPasta", 4 | "displayName": "Easy Paste", 5 | "version": "2.2.0", 6 | "publisher": "DargonLee", 7 | "description": "跨平台剪贴板工具" 8 | }, 9 | "build": { 10 | "outputDir": "build", 11 | "clean": true, 12 | "debug": false 13 | }, 14 | "platforms": { 15 | "macos": { 16 | "bundleId": "com.harlans.easypasta.app", 17 | "category": "public.app-category.utilities", 18 | "minVersion": "10.14", 19 | "outputFormat": "dmg" 20 | }, 21 | "windows": { 22 | "arch": "x64", 23 | "outputFormat": "exe", 24 | "installerType": "nsis" 25 | }, 26 | "linux": { 27 | "arch": "x64", 28 | "outputFormat": "deb", 29 | "dependencies": [ 30 | "libgtk-3-0", 31 | "libblkid1" 32 | ] 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | CONFIG_FILE="$SCRIPT_DIR/common/config.json" 5 | 6 | source "$SCRIPT_DIR/common/utils.sh" 7 | 8 | main() { 9 | # 检查配置文件 10 | if [ ! -f "$CONFIG_FILE" ]; then 11 | error "配置文件不存在: $CONFIG_FILE" 12 | exit 1 13 | fi 14 | 15 | # 检查平台 16 | case "$(uname -s)" in 17 | Darwin*) 18 | source "$SCRIPT_DIR/platforms/macos/build.sh" 19 | build_macos "$CONFIG_FILE" 20 | ;; 21 | Linux*) 22 | source "$SCRIPT_DIR/platforms/linux/build.sh" 23 | build_linux "$CONFIG_FILE" 24 | ;; 25 | *) 26 | error "不支持的操作系统" 27 | exit 1 28 | ;; 29 | esac 30 | } 31 | 32 | main "$@" 33 | 34 | 35 | # Unix系统 36 | # ./scripts/build.sh 37 | 38 | # Windows系统 39 | # scripts\build.bat -------------------------------------------------------------------------------- /lib/model/clipboard_type.dart: -------------------------------------------------------------------------------- 1 | /// 剪贴板内容类型 2 | enum ClipboardType { 3 | text('text'), // 文本 4 | image('image'), // 图片 5 | file('file'), // 文件 6 | url('url'), // 链接 7 | html('html'), // HTML 8 | rtf('rtf'), // RTF 9 | unknown('unknown'); // 未知类型 10 | 11 | final String value; 12 | const ClipboardType(this.value); 13 | 14 | /// 从字符串转换为枚举 15 | static ClipboardType fromString(String? value) { 16 | switch (value) { 17 | case 'text': 18 | return ClipboardType.text; 19 | case 'image': 20 | return ClipboardType.image; 21 | case 'file': 22 | return ClipboardType.file; 23 | case 'url': 24 | return ClipboardType.url; 25 | case 'html': 26 | return ClipboardType.html; 27 | case 'rtf': 28 | return ClipboardType.rtf; 29 | default: 30 | return ClipboardType.unknown; 31 | } 32 | } 33 | 34 | @override 35 | String toString() => value; 36 | } 37 | -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/model/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 应用主题配置 4 | class AppTheme { 5 | /// 亮色主题 6 | static ThemeData light() => ThemeData( 7 | colorScheme: const ColorScheme.light( 8 | primary: Colors.blue, 9 | secondary: Colors.blueAccent, 10 | surface: Colors.white, 11 | ), 12 | scaffoldBackgroundColor: Colors.white, 13 | cardColor: Colors.white, 14 | dividerColor: Colors.grey[200]!, 15 | useMaterial3: true, 16 | ); 17 | 18 | /// 暗色主题 19 | static ThemeData dark() => ThemeData( 20 | colorScheme: const ColorScheme.dark( 21 | primary: Colors.blue, 22 | secondary: Colors.blueAccent, 23 | surface: Color(0xFF1E1E1E), 24 | ), 25 | scaffoldBackgroundColor: const Color(0xFF121212), 26 | cardColor: const Color(0xFF1E1E1E), 27 | dividerColor: Colors.grey[800]!, 28 | useMaterial3: true, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/widget/cards/image_card.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ImageContent extends StatelessWidget { 5 | final Uint8List imageBytes; 6 | final double borderRadius; 7 | final BoxFit fit; 8 | 9 | const ImageContent({ 10 | Key? key, 11 | required this.imageBytes, 12 | this.borderRadius = 8.0, 13 | this.fit = BoxFit.cover, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ClipRRect( 19 | borderRadius: BorderRadius.circular(borderRadius), 20 | child: Image.memory( 21 | imageBytes, 22 | fit: fit, 23 | errorBuilder: (context, error, stackTrace) { 24 | return Center( 25 | child: Icon( 26 | Icons.broken_image_outlined, 27 | color: Colors.grey[400], 28 | size: 32, 29 | ), 30 | ); 31 | }, 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/core/icon_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/model/clipboard_type.dart'; 3 | 4 | /// 类型图标助手类 5 | /// 根据剪贴板内容类型返回对应的图标 6 | class TypeIconHelper { 7 | static final _urlPattern = RegExp( 8 | r'^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$', 9 | caseSensitive: false, 10 | ); 11 | 12 | static IconData getTypeIcon(ClipboardType type, {String? pvalue}) { 13 | if (pvalue == null) { 14 | return Icons.content_copy; 15 | } 16 | 17 | switch (type) { 18 | case ClipboardType.text: 19 | return _urlPattern.hasMatch(pvalue) ? Icons.link : Icons.text_fields; 20 | case ClipboardType.image: 21 | return Icons.image; 22 | case ClipboardType.file: 23 | return pvalue.endsWith('/') ? Icons.folder : Icons.insert_drive_file; 24 | case ClipboardType.html: 25 | return Icons.code; 26 | default: 27 | return Icons.content_copy; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widget/search_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SearchTextField extends StatelessWidget { 4 | final TextEditingController controller; 5 | final Function(String) onChanged; 6 | final VoidCallback onClear; 7 | 8 | const SearchTextField({ 9 | super.key, 10 | required this.controller, 11 | required this.onChanged, 12 | required this.onClear, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return TextField( 18 | controller: controller, 19 | onChanged: onChanged, 20 | decoration: InputDecoration( 21 | hintText: '搜索...', 22 | prefixIcon: const Icon(Icons.search, color: Colors.red), 23 | suffixIcon: IconButton( 24 | icon: const Icon(Icons.clear), 25 | onPressed: onClear, 26 | ), 27 | border: OutlineInputBorder( 28 | borderRadius: BorderRadius.circular(8), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: easy_pasta 2 | description: A cross-platform clipboard manager for macOS, Linux and Windows, providing convenient copy and paste functionality. 3 | publish_to: "none" 4 | version: 2.2.0+1 5 | package_name: com.harlans.easypasta.app 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | cupertino_icons: ^1.0.2 12 | window_manager: ^0.4.3 13 | hotkey_manager: ^0.2.3 14 | super_clipboard: ^0.8.24 15 | tray_manager: ^0.3.1 16 | launch_at_startup: ^0.3.1 17 | package_info_plus: ^8.1.2 18 | intl: ^0.20.1 19 | flutter_html: ^3.0.0-beta.2 20 | flutter_highlight: ^0.7.0 21 | url_launcher: ^6.3.1 22 | provider: ^6.1.2 23 | path_provider: ^2.1.5 24 | sqflite_common_ffi: ^2.3.4+4 25 | shared_preferences: ^2.3.4 26 | crypto: ^3.0.6 27 | uuid: ^4.5.1 28 | bonsoir: ^5.1.11 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | flutter_lints: ^2.0.0 34 | flutter: 35 | uses-material-design: true 36 | assets: 37 | - assets/images/ 38 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | hotkey_manager_linux 7 | irondash_engine_context 8 | screen_retriever_linux 9 | super_native_extensions 10 | tray_manager 11 | url_launcher_linux 12 | window_manager 13 | ) 14 | 15 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 16 | ) 17 | 18 | set(PLUGIN_BUNDLED_LIBRARIES) 19 | 20 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 22 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 25 | endforeach(plugin) 26 | 27 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 28 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 30 | endforeach(ffi_plugin) 31 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | bonsoir_windows 7 | hotkey_manager_windows 8 | irondash_engine_context 9 | screen_retriever_windows 10 | super_native_extensions 11 | tray_manager 12 | url_launcher_windows 13 | window_manager 14 | ) 15 | 16 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 17 | ) 18 | 19 | set(PLUGIN_BUNDLED_LIBRARIES) 20 | 21 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 26 | endforeach(plugin) 27 | 28 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 29 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 31 | endforeach(ffi_plugin) 32 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /lib/widget/cards/source_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_highlight/flutter_highlight.dart'; 3 | import 'package:flutter_highlight/themes/github.dart'; 4 | 5 | class SourceCodeContent extends StatelessWidget { 6 | final String code; 7 | final bool isSelected; 8 | final VoidCallback? onTap; 9 | 10 | const SourceCodeContent({ 11 | Key? key, 12 | required this.code, 13 | this.isSelected = false, 14 | this.onTap, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | const language = 'dart'; 20 | 21 | return ConstrainedBox( 22 | constraints: const BoxConstraints( 23 | maxWidth: double.infinity, 24 | maxHeight: double.infinity, 25 | ), 26 | child: HighlightView( 27 | code, 28 | language: language, 29 | theme: githubTheme, 30 | padding: const EdgeInsets.all(12), 31 | textStyle: const TextStyle( 32 | fontSize: 14, 33 | fontFamily: 'monospace', 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/common/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 导入日志工具 4 | source "$(dirname "${BASH_SOURCE[0]}")/logger.sh" 5 | 6 | # 检查命令是否存在 7 | check_command() { 8 | if ! command -v "$1" &> /dev/null; then 9 | error "命令 '$1' 未找到,请先安装" 10 | return 1 11 | fi 12 | } 13 | 14 | # 检查依赖工具 15 | check_dependencies() { 16 | local deps=("$@") 17 | local missing=() 18 | 19 | for dep in "${deps[@]}"; do 20 | if ! check_command "$dep"; then 21 | missing+=("$dep") 22 | fi 23 | done 24 | 25 | if [ ${#missing[@]} -ne 0 ]; then 26 | error "缺少必要的依赖: ${missing[*]}" 27 | return 1 28 | fi 29 | } 30 | 31 | # 清理构建目录 32 | clean_build_dir() { 33 | local build_dir=$1 34 | if [ -d "$build_dir" ]; then 35 | info "清理构建目录: $build_dir" 36 | rm -rf "$build_dir" 37 | fi 38 | } 39 | 40 | # 读取配置文件 41 | read_config() { 42 | local config_file=$1 43 | local key=$2 44 | 45 | if [ ! -f "$config_file" ]; then 46 | error "配置文件不存在: $config_file" 47 | return 1 48 | fi 49 | 50 | jq -r "$key" "$config_file" 51 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Harlans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/page/confirm_dialog_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ConfirmDialog extends StatelessWidget { 4 | final String title; 5 | final String content; 6 | final String confirmText; 7 | final String cancelText; 8 | final Color? confirmColor; 9 | final Color? cancelColor; 10 | 11 | const ConfirmDialog({ 12 | super.key, 13 | required this.title, 14 | required this.content, 15 | this.confirmText = '确定', 16 | this.cancelText = '取消', 17 | this.confirmColor = Colors.red, 18 | this.cancelColor = Colors.grey, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return AlertDialog( 24 | title: Text(title), 25 | content: Text(content), 26 | actions: [ 27 | TextButton( 28 | onPressed: () => Navigator.pop(context, false), 29 | child: Text(cancelText, style: TextStyle(color: cancelColor)), 30 | ), 31 | TextButton( 32 | onPressed: () => Navigator.pop(context, true), 33 | child: Text(confirmText, style: TextStyle(color: confirmColor)), 34 | ), 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:easy_pasta/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/core/html_processor.dart: -------------------------------------------------------------------------------- 1 | class HtmlProcessor { 2 | /// 处理 HTML 内容 3 | /// - 移除背景色 4 | /// - 移除多余的换行和空格 5 | /// - 保持格式化的同时简化 HTML 结构 6 | static String processHtml(String html) { 7 | String processed = html; 8 | 9 | // 替换背景色 10 | processed = processed.replaceAll( 11 | 'background-color: #ffffff;', 'background-color: transparent;'); 12 | return processed; 13 | } 14 | 15 | static String processHtml2(String html) { 16 | // 使用正则表达式匹配每一行的内容,包括样式 17 | final regex = 18 | RegExp(r'
]*>([^<]*)([^<]*)
', multiLine: true); 19 | 20 | // 收集所有行,保留样式信息 21 | var processedLines = regex.allMatches(html).map((match) { 22 | // 获取整行的内容 23 | var line = match.group(0) ?? ''; 24 | // 将div替换为span,这样就不会产生换行 25 | return line 26 | .replaceAll('
', '') 27 | .replaceAll('
', '
'); 28 | }).join(''); 29 | 30 | // 包装处理后的内容 31 | return ''' 32 |
33 |       
34 |         $processedLines
35 |       
36 |     
37 | '''; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/core/window_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:window_manager/window_manager.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class WindowService { 5 | static final WindowService _instance = WindowService._internal(); 6 | factory WindowService() => _instance; 7 | WindowService._internal() { 8 | init(); 9 | } 10 | 11 | Future init() async { 12 | await windowManager.ensureInitialized(); 13 | 14 | WindowOptions windowOptions = const WindowOptions( 15 | size: Size(950, 680), 16 | minimumSize: Size(370, 680), 17 | center: true, 18 | titleBarStyle: TitleBarStyle.hidden, 19 | windowButtonVisibility: false, 20 | ); 21 | windowManager.waitUntilReadyToShow(windowOptions, () async {}); 22 | } 23 | 24 | Future showWindow() async { 25 | await windowManager.show(); 26 | await windowManager.focus(); 27 | await windowManager.setAlwaysOnTop(true); 28 | await Future.delayed(const Duration(milliseconds: 100)); 29 | await windowManager.setAlwaysOnTop(false); 30 | } 31 | 32 | Future hideWindow() async { 33 | await windowManager.hide(); 34 | } 35 | 36 | Future closeWindow() async { 37 | await windowManager.close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | NSHumanReadableCopyright 28 | $(PRODUCT_COPYRIGHT) 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "filename" : "icon.png", 50 | "idiom" : "mac", 51 | "scale" : "2x", 52 | "size" : "512x512" 53 | } 54 | ], 55 | "info" : { 56 | "author" : "xcode", 57 | "version" : 1 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/common/logger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 颜色定义 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[1;33m' 7 | BLUE='\033[0;34m' 8 | NC='\033[0m' 9 | 10 | # 日志级别 11 | LOG_LEVEL_DEBUG=0 12 | LOG_LEVEL_INFO=1 13 | LOG_LEVEL_WARN=2 14 | LOG_LEVEL_ERROR=3 15 | 16 | # 当前日志级别 17 | CURRENT_LOG_LEVEL=$LOG_LEVEL_INFO 18 | 19 | log() { 20 | local level=$1 21 | local message=$2 22 | local timestamp=$(date '+%Y-%m-%d %H:%M:%S') 23 | 24 | if [ $level -ge $CURRENT_LOG_LEVEL ]; then 25 | case $level in 26 | $LOG_LEVEL_DEBUG) 27 | echo -e "${BLUE}[DEBUG]${NC} ${timestamp} - ${message}" 28 | ;; 29 | $LOG_LEVEL_INFO) 30 | echo -e "${GREEN}[INFO]${NC} ${timestamp} - ${message}" 31 | ;; 32 | $LOG_LEVEL_WARN) 33 | echo -e "${YELLOW}[WARN]${NC} ${timestamp} - ${message}" 34 | ;; 35 | $LOG_LEVEL_ERROR) 36 | echo -e "${RED}[ERROR]${NC} ${timestamp} - ${message}" 37 | ;; 38 | esac 39 | fi 40 | } 41 | 42 | debug() { log $LOG_LEVEL_DEBUG "$1"; } 43 | info() { log $LOG_LEVEL_INFO "$1"; } 44 | warn() { log $LOG_LEVEL_WARN "$1"; } 45 | error() { log $LOG_LEVEL_ERROR "$1"; } -------------------------------------------------------------------------------- /scripts/platforms/macos/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../../common/utils.sh" 4 | 5 | build_macos() { 6 | local config_file=$1 7 | 8 | # 检查依赖 9 | check_dependencies "flutter" "create-dmg" || exit 1 10 | 11 | # 读取配置 12 | local app_name=$(read_config "$config_file" '.app.name') 13 | local version=$(read_config "$config_file" '.app.version') 14 | local output_dir=$(read_config "$config_file" '.build.outputDir') 15 | local root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)" 16 | 17 | info "开始构建 macOS 版本..." 18 | 19 | # 构建应用 20 | flutter build macos --release || { 21 | error "macOS构建失败" 22 | return 1 23 | } 24 | 25 | # 创建DMG 26 | info "创建DMG安装包..." 27 | create-dmg \ 28 | --volname "$app_name" \ 29 | --volicon "$root_dir/assets/images/tray_icon_original.icns" \ 30 | --window-pos 200 120 \ 31 | --window-size 800 400 \ 32 | --icon-size 100 \ 33 | --app-drop-link 600 185 \ 34 | "$root_dir/$output_dir/macos/$app_name-$version.dmg" \ 35 | "$root_dir/$output_dir/macos/Build/Products/Release/$app_name.app" || { 36 | error "DMG创建失败" 37 | return 1 38 | } 39 | 40 | info "macOS构建完成" 41 | } -------------------------------------------------------------------------------- /lib/core/dynamic_content_hash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | import 'package:crypto/crypto.dart'; 4 | 5 | class DynamicContentHash { 6 | // 方法1: 使用内容特征生成hash 7 | static String generateContentHash(dynamic content) { 8 | final timestamp = DateTime.now().millisecondsSinceEpoch; 9 | final contentString = content.toString(); 10 | final input = utf8.encode('$timestamp-$contentString'); 11 | 12 | return sha256.convert(input).toString(); 13 | } 14 | 15 | // 方法2: 时间戳+随机数 16 | static String generateTimestampHash() { 17 | final timestamp = DateTime.now().millisecondsSinceEpoch; 18 | final random = (100000 + Random().nextInt(900000)).toString(); 19 | 20 | return '$timestamp-$random'; 21 | } 22 | 23 | // 方法3: 针对特定内容结构 24 | static String generateStructuredHash(Map content) { 25 | // 提取关键字段 26 | final id = content['id'] ?? ''; 27 | final title = content['title'] ?? ''; 28 | final updateTime = content['updateTime'] ?? ''; 29 | 30 | final input = utf8.encode('$id-$title-$updateTime'); 31 | return md5.convert(input).toString(); 32 | } 33 | 34 | // 方法4: 版本化的hash 35 | static String generateVersionHash(dynamic content, String version) { 36 | final input = utf8.encode('$content-v$version'); 37 | return sha1.convert(input).toString(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | echo 开始构建 Windows 版本... 5 | 6 | :: 清理旧的构建文件 7 | echo 清理旧的构建文件... 8 | if exist "build\windows" rd /s /q "build\windows" 9 | 10 | :: 获取Flutter依赖 11 | echo 获取Flutter依赖... 12 | call flutter pub get 13 | 14 | :: 构建应用 15 | echo 构建应用... 16 | call flutter build windows --release 17 | 18 | :: 创建安装包(使用NSIS) 19 | if exist "C:\Program Files (x86)\NSIS\makensis.exe" ( 20 | echo 创建安装包... 21 | 22 | :: 创建NSIS脚本 23 | ( 24 | echo !define PRODUCT_NAME "Easy Paste" 25 | echo !define PRODUCT_VERSION "1.0.0" 26 | echo !define PRODUCT_PUBLISHER "Your Company" 27 | 28 | echo Name "${PRODUCT_NAME}" 29 | echo OutFile "build\windows\EasyPaste-Setup.exe" 30 | echo InstallDir "$PROGRAMFILES64\Easy Paste" 31 | 32 | echo Section "MainSection" SEC01 33 | echo SetOutPath "$INSTDIR" 34 | echo File /r "build\windows\runner\Release\*.*" 35 | echo CreateDirectory "$SMPROGRAMS\Easy Paste" 36 | echo CreateShortCut "$SMPROGRAMS\Easy Paste\Easy Paste.lnk" "$INSTDIR\easy_paste.exe" 37 | echo SectionEnd 38 | ) > "build\windows\installer.nsi" 39 | 40 | "C:\Program Files (x86)\NSIS\makensis.exe" "build\windows\installer.nsi" 41 | ) 42 | 43 | echo Windows 版本构建完成! 44 | 45 | @REM scripts\build.bat -------------------------------------------------------------------------------- /scripts/platforms/linux/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/../../common/utils.sh" 4 | 5 | build_linux() { 6 | local config_file=$1 7 | 8 | # 检查依赖 9 | check_dependencies "flutter" "dpkg-deb" || exit 1 10 | 11 | # 读取配置 12 | local app_name=$(read_config "$config_file" '.app.name') 13 | local version=$(read_config "$config_file" '.app.version') 14 | local output_dir=$(read_config "$config_file" '.build.outputDir') 15 | 16 | info "开始构建 Linux 版本..." 17 | 18 | # 构建应用 19 | flutter build linux --release || { 20 | error "Linux构建失败" 21 | return 1 22 | } 23 | 24 | # 创建deb包 25 | info "创建DEB安装包..." 26 | create_deb_package "$app_name" "$version" "$output_dir/linux" || { 27 | error "DEB包创建失败" 28 | return 1 29 | } 30 | 31 | info "Linux构建完成" 32 | } 33 | 34 | create_deb_package() { 35 | local app_name=$1 36 | local version=$2 37 | local output_dir=$3 38 | 39 | mkdir -p "$output_dir/debian/DEBIAN" 40 | cat > "$output_dir/debian/DEBIAN/control" << EOF 41 | Package: $app_name 42 | Version: $version 43 | Section: utils 44 | Priority: optional 45 | Architecture: amd64 46 | Maintainer: $(read_config "$config_file" '.app.publisher') 47 | Description: $(read_config "$config_file" '.app.description') 48 | EOF 49 | 50 | dpkg-deb --build "$output_dir/debian" "$output_dir/$app_name-$version.deb" 51 | } -------------------------------------------------------------------------------- /lib/core/startup_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:launch_at_startup/launch_at_startup.dart'; 2 | import 'package:package_info_plus/package_info_plus.dart'; 3 | import 'package:easy_pasta/db/shared_preference_helper.dart'; 4 | import 'dart:io' show Platform; 5 | 6 | class StartupService { 7 | static final StartupService _instance = StartupService._internal(); 8 | factory StartupService() => _instance; 9 | StartupService._internal() { 10 | init(); 11 | } 12 | 13 | Future init() async { 14 | final prefs = await SharedPreferenceHelper.instance; 15 | final enable = prefs.getLoginInLaunch(); 16 | setEnable(enable); 17 | } 18 | 19 | Future setEnable(bool enable) async { 20 | final prefs = await SharedPreferenceHelper.instance; 21 | await prefs.setLoginInLaunch(enable); 22 | final packageInfo = await PackageInfo.fromPlatform(); 23 | launchAtStartup.setup( 24 | appName: packageInfo.appName, 25 | appPath: Platform.resolvedExecutable, 26 | packageName: 'dev.harlans.easy_pasta', 27 | ); 28 | if (enable) { 29 | await launchAtStartup.enable(); 30 | } else { 31 | await launchAtStartup.disable(); 32 | } 33 | } 34 | 35 | Future enable() async { 36 | await launchAtStartup.enable(); 37 | } 38 | 39 | Future disable() async { 40 | await launchAtStartup.disable(); 41 | } 42 | 43 | Future isEnabled() async { 44 | return await launchAtStartup.isEnabled(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.15' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/widget/setting_tiles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/model/settings_model.dart'; 3 | import 'package:easy_pasta/core/settings_service.dart'; 4 | import 'package:hotkey_manager/hotkey_manager.dart'; 5 | 6 | class HotkeyTile extends StatelessWidget { 7 | final SettingItem item; 8 | final HotKey? hotKey; 9 | final ValueChanged onHotKeyChanged; 10 | 11 | const HotkeyTile({ 12 | Key? key, 13 | required this.item, 14 | required this.hotKey, 15 | required this.onHotKeyChanged, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ListTile( 21 | leading: Icon(item.icon, color: item.iconColor), 22 | title: Text(item.title), 23 | subtitle: Text(item.subtitle), 24 | onTap: () async { 25 | // Handle hotkey change logic 26 | }, 27 | ); 28 | } 29 | } 30 | 31 | class ThemeTile extends StatelessWidget { 32 | final SettingItem item; 33 | final ThemeMode currentThemeMode; 34 | final ValueChanged onThemeModeChanged; 35 | 36 | const ThemeTile({ 37 | Key? key, 38 | required this.item, 39 | required this.currentThemeMode, 40 | required this.onThemeModeChanged, 41 | }) : super(key: key); 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return ListTile( 46 | leading: Icon(item.icon, color: item.iconColor), 47 | title: Text(item.title), 48 | subtitle: Text(item.subtitle), 49 | onTap: () { 50 | // Handle theme change logic 51 | }, 52 | ); 53 | } 54 | } -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "flutter_window.h" 7 | #include "utils.h" 8 | 9 | auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); 10 | 11 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 12 | _In_ wchar_t *command_line, _In_ int show_command) { 13 | // Attach to console when present (e.g., 'flutter run') or create a 14 | // new console when running with a debugger. 15 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 16 | CreateAndAttachConsole(); 17 | } 18 | 19 | // Initialize COM, so that it is available for use in the library and/or 20 | // plugins. 21 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 22 | 23 | flutter::DartProject project(L"data"); 24 | 25 | std::vector command_line_arguments = 26 | GetCommandLineArguments(); 27 | 28 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 29 | 30 | FlutterWindow window(project); 31 | Win32Window::Point origin(10, 10); 32 | Win32Window::Size size(1280, 720); 33 | if (!window.Create(L"easy_pasta", origin, size)) { 34 | return EXIT_FAILURE; 35 | } 36 | window.SetQuitOnClose(true); 37 | 38 | ::MSG msg; 39 | while (::GetMessage(&msg, nullptr, 0, 0)) { 40 | ::TranslateMessage(&msg); 41 | ::DispatchMessage(&msg); 42 | } 43 | 44 | ::CoUninitialize(); 45 | return EXIT_SUCCESS; 46 | } 47 | -------------------------------------------------------------------------------- /lib/core/hotkey_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:hotkey_manager/hotkey_manager.dart'; 3 | import 'package:easy_pasta/db/shared_preference_helper.dart'; 4 | import 'package:easy_pasta/core/window_service.dart'; 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | 8 | class HotkeyService { 9 | // 单例模式 10 | static final HotkeyService _instance = HotkeyService._internal(); 11 | factory HotkeyService() => _instance; 12 | HotkeyService._internal() { 13 | init(); 14 | } 15 | 16 | Future init() async { 17 | await hotKeyManager.unregisterAll(); 18 | final prefs = await SharedPreferenceHelper.instance; 19 | final hotkey = prefs.getShortcutKey(); 20 | if (hotkey.isEmpty) return; 21 | 22 | final hotKey = HotKey.fromJson(json.decode(hotkey)); 23 | await setHotkey(hotKey); 24 | await setCloseWindowHotkey(); 25 | } 26 | 27 | Future setCloseWindowHotkey() async { 28 | HotKey hotKey = _getCloseWindowHotKey(); 29 | await hotKeyManager.register(hotKey, keyDownHandler: (hotKey) { 30 | WindowService().closeWindow(); 31 | }); 32 | } 33 | 34 | HotKey _getCloseWindowHotKey() { 35 | return HotKey( 36 | key: const PhysicalKeyboardKey(0x0007001a), 37 | modifiers: 38 | Platform.isWindows ? [HotKeyModifier.control] : [HotKeyModifier.meta], 39 | scope: HotKeyScope.inapp, 40 | ); 41 | } 42 | 43 | // 注册新的热键 44 | Future setHotkey(HotKey hotkey) async { 45 | await hotKeyManager.register( 46 | hotkey, 47 | keyDownHandler: (hotKey) { 48 | WindowService().showWindow(); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/core/settings_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hotkey_manager/hotkey_manager.dart'; 3 | import 'package:easy_pasta/providers/pboard_provider.dart'; 4 | import 'package:easy_pasta/db/shared_preference_helper.dart'; 5 | import 'package:easy_pasta/core/hotkey_service.dart'; 6 | import 'package:easy_pasta/core/startup_service.dart'; 7 | import 'dart:convert'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class SettingsService { 11 | static final SettingsService _instance = SettingsService._internal(); 12 | factory SettingsService() => _instance; 13 | SettingsService._internal(); 14 | 15 | final _prefs = SharedPreferenceHelper.instance; 16 | final _startupService = StartupService(); 17 | final _hotkeyService = HotkeyService(); 18 | 19 | Future setHotKey(HotKey hotKey) async { 20 | await _hotkeyService.setHotkey(hotKey); 21 | await _prefs 22 | .then((prefs) => prefs.setShortcutKey(json.encode(hotKey.toJson()))); 23 | } 24 | 25 | Future getHotKey() async { 26 | final prefs = await _prefs; 27 | final hotkey = prefs.getShortcutKey(); 28 | return hotkey.isNotEmpty ? HotKey.fromJson(json.decode(hotkey)) : null; 29 | } 30 | 31 | Future setAutoLaunch(bool value) async { 32 | await _startupService.setEnable(value); 33 | await _prefs.then((prefs) => prefs.setLoginInLaunch(value)); 34 | } 35 | 36 | Future getAutoLaunch() async { 37 | final prefs = await _prefs; 38 | return prefs.getLoginInLaunch(); 39 | } 40 | 41 | Future clearAllData(BuildContext context) async { 42 | context.read().clearAll(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/widget/cards/html_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | 5 | class HtmlContent extends StatelessWidget { 6 | static final Map _defaultStyles = { 7 | "body": Style( 8 | margin: Margins.zero, 9 | padding: HtmlPaddings.zero, 10 | width: Width.auto(), 11 | ), 12 | "pre": Style( 13 | margin: Margins.zero, 14 | padding: HtmlPaddings.zero, 15 | fontFamily: "monospace", 16 | backgroundColor: Colors.transparent, 17 | ), 18 | "code": Style( 19 | margin: Margins.zero, 20 | padding: HtmlPaddings.zero, 21 | display: Display.block, 22 | backgroundColor: Colors.transparent, 23 | ), 24 | "span": Style( 25 | lineHeight: const LineHeight(1.2), 26 | margin: Margins.zero, 27 | padding: HtmlPaddings.zero, 28 | backgroundColor: Colors.transparent, 29 | ), 30 | "div": Style( 31 | margin: Margins.zero, 32 | padding: HtmlPaddings.zero, 33 | backgroundColor: Colors.transparent, 34 | ), 35 | }; 36 | 37 | final String htmlData; 38 | 39 | const HtmlContent({ 40 | super.key, 41 | required this.htmlData, 42 | }); 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return Html( 47 | shrinkWrap: true, 48 | data: htmlData.length > 800 ? htmlData.substring(0, 800) : htmlData, 49 | style: _defaultStyles, 50 | onLinkTap: (url, _, __) { 51 | if (url != null) { 52 | launchUrl(Uri.parse(url)); 53 | } 54 | }, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import window_manager 4 | import LaunchAtLogin 5 | 6 | class MainFlutterWindow: NSWindow { 7 | 8 | // MARK: - Lifecycle 9 | override func awakeFromNib() { 10 | configureWindow() 11 | super.awakeFromNib() 12 | } 13 | 14 | override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { 15 | super.order(place, relativeTo: otherWin) 16 | hiddenWindowAtLaunch() 17 | } 18 | 19 | // MARK: - Window Configuration 20 | private func configureWindow() { 21 | let flutterViewController = FlutterViewController() 22 | let windowFrame = self.frame 23 | self.contentViewController = flutterViewController 24 | self.setFrame(windowFrame, display: true) 25 | 26 | FlutterMethodChannel( 27 | name: "launch_at_startup", binaryMessenger: flutterViewController.engine.binaryMessenger 28 | ) 29 | .setMethodCallHandler { (_ call: FlutterMethodCall, result: @escaping FlutterResult) in 30 | switch call.method { 31 | case "launchAtStartupIsEnabled": 32 | result(LaunchAtLogin.isEnabled) 33 | case "launchAtStartupSetEnabled": 34 | if let arguments = call.arguments as? [String: Any] { 35 | LaunchAtLogin.isEnabled = arguments["setEnabledValue"] as! Bool 36 | } 37 | result(nil) 38 | default: 39 | result(FlutterMethodNotImplemented) 40 | } 41 | } 42 | 43 | RegisterGeneratedPlugins(registry: flutterViewController) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void RegisterPlugins(flutter::PluginRegistry* registry) { 19 | BonsoirWindowsPluginCApiRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi")); 21 | HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); 23 | IrondashEngineContextPluginCApiRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); 25 | ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); 27 | SuperNativeExtensionsPluginCApiRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); 29 | TrayManagerPluginRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("TrayManagerPlugin")); 31 | UrlLauncherWindowsRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 33 | WindowManagerPluginRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 35 | } 36 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import bonsoir_darwin 9 | import device_info_plus 10 | import hotkey_manager_macos 11 | import irondash_engine_context 12 | import package_info_plus 13 | import path_provider_foundation 14 | import screen_retriever_macos 15 | import shared_preferences_foundation 16 | import super_native_extensions 17 | import tray_manager 18 | import url_launcher_macos 19 | import window_manager 20 | 21 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 22 | SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin")) 23 | DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 24 | HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) 25 | IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) 26 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 27 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 28 | ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) 29 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 30 | SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) 31 | TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) 32 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 33 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 34 | } 35 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 17 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 18 | - platform: android 19 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 20 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 21 | - platform: ios 22 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 23 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 24 | - platform: linux 25 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 26 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 27 | - platform: macos 28 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 29 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 30 | - platform: web 31 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 32 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 33 | - platform: windows 34 | create_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 35 | base_revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /scripts/platforms/windows/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | call :init 5 | call :check_dependencies || exit /b 1 6 | call :build || exit /b 1 7 | call :create_installer || exit /b 1 8 | exit /b 0 9 | 10 | :init 11 | set "SCRIPT_DIR=%~dp0" 12 | set "CONFIG_FILE=%SCRIPT_DIR%\..\..\common\config.json" 13 | exit /b 0 14 | 15 | :check_dependencies 16 | where flutter >nul 2>&1 || ( 17 | echo [ERROR] Flutter not found 18 | exit /b 1 19 | ) 20 | exit /b 0 21 | 22 | :build 23 | echo [INFO] Building Windows version... 24 | call flutter build windows --release 25 | if errorlevel 1 ( 26 | echo [ERROR] Windows build failed 27 | exit /b 1 28 | ) 29 | exit /b 0 30 | 31 | :create_installer 32 | if exist "C:\Program Files (x86)\NSIS\makensis.exe" ( 33 | echo [INFO] Creating installer... 34 | call :generate_nsis_script 35 | "C:\Program Files (x86)\NSIS\makensis.exe" "build\windows\installer.nsi" 36 | ) else ( 37 | echo [WARN] NSIS not found, skipping installer creation 38 | ) 39 | exit /b 0 40 | 41 | :generate_nsis_script 42 | ( 43 | echo !define PRODUCT_NAME "Easy Paste" 44 | echo !define PRODUCT_VERSION "1.0.0" 45 | echo !define PRODUCT_PUBLISHER "Your Company" 46 | echo Name "${PRODUCT_NAME}" 47 | echo OutFile "build\windows\EasyPaste-Setup.exe" 48 | echo InstallDir "$PROGRAMFILES64\Easy Paste" 49 | echo Section "MainSection" SEC01 50 | echo SetOutPath "$INSTDIR" 51 | echo File /r "build\windows\runner\Release\*.*" 52 | echo CreateDirectory "$SMPROGRAMS\Easy Paste" 53 | echo CreateShortCut "$SMPROGRAMS\Easy Paste\Easy Paste.lnk" "$INSTDIR\easy_paste.exe" 54 | echo SectionEnd 55 | ) > "build\windows\installer.nsi" 56 | exit /b 0 -------------------------------------------------------------------------------- /lib/model/settings_constants.dart: -------------------------------------------------------------------------------- 1 | class SettingsConstants { 2 | static const String aboutTitle = '关于'; 3 | static const String appVersion = 'v2.2.0'; 4 | static const String githubUrl = 'https://github.com/DargonLee/easy_pasta'; 5 | static const String versionInfoTitle = '版本信息'; 6 | static const String versionInfoSubtitle = '查看版本和项目信息'; 7 | 8 | static const String basicSettingsTitle = '基本设置'; 9 | // 快捷键 10 | static const String hotkeyTitle = '快捷键'; 11 | static const String hotkeySubtitle = '设置全局快捷键'; 12 | 13 | // 主题 14 | static const String themeTitle = '主题'; 15 | static const String themeSubtitle = '设置应用主题'; 16 | 17 | // 开机自启 18 | static const String autoLaunchTitle = '开机自启'; 19 | static const String autoLaunchSubtitle = '系统启动时自动运行'; 20 | 21 | // 最大存储 22 | static const String maxStorageTitle = '最大存储'; 23 | static const String maxStorageSubtitle = '设置最大存储条数'; 24 | 25 | // 启用Bonjour 26 | static const String bonjourTitle = '启用Bonjour'; 27 | static const String bonjourSubtitle = '测试Bonjour功能'; 28 | 29 | // 清除记录 30 | static const String clearDataTitle = '清除记录'; 31 | static const String clearDataSubtitle = '删除所有剪贴板记录'; 32 | 33 | // 退出应用 34 | static const String exitAppTitle = '退出应用'; 35 | static const String exitAppSubtitle = '完全退出应用程序'; 36 | 37 | // Dialog texts 38 | static const String clearConfirmTitle = '确认清除'; 39 | static const String clearConfirmContent = '是否清除所有剪贴板记录?此操作不可恢复。'; 40 | static const String exitConfirmTitle = '确认退出'; 41 | static const String exitConfirmContent = '确定要退出应用吗?'; 42 | static const String resetConfirmTitle = '确认重置'; 43 | static const String resetConfirmContent = '确定要重置应用吗?'; 44 | 45 | // Button texts 46 | static const String confirmText = '确定'; 47 | static const String cancelText = '取消'; 48 | static const String modifyText = '修改'; 49 | static const String setUpText = '设置'; 50 | } 51 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | return true; 35 | } 36 | 37 | void FlutterWindow::OnDestroy() { 38 | if (flutter_controller_) { 39 | flutter_controller_ = nullptr; 40 | } 41 | 42 | Win32Window::OnDestroy(); 43 | } 44 | 45 | LRESULT 46 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 47 | WPARAM const wparam, 48 | LPARAM const lparam) noexcept { 49 | // Give Flutter, including plugins, an opportunity to handle window messages. 50 | if (flutter_controller_) { 51 | std::optional result = 52 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 53 | lparam); 54 | if (result) { 55 | return *result; 56 | } 57 | } 58 | 59 | switch (message) { 60 | case WM_FONTCHANGE: 61 | flutter_controller_->engine()->ReloadSystemFonts(); 62 | break; 63 | } 64 | 65 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 66 | } 67 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_pasta/providers/pboard_provider.dart'; 2 | import 'package:easy_pasta/page/home_page_view.dart'; 3 | import 'package:easy_pasta/page/bonsoir_page.dart'; 4 | import 'package:easy_pasta/core/tray_service.dart'; 5 | import 'package:easy_pasta/core/window_service.dart'; 6 | import 'package:easy_pasta/core/hotkey_service.dart'; 7 | import 'package:easy_pasta/core/startup_service.dart'; 8 | import 'package:easy_pasta/providers/theme_provider.dart'; 9 | import 'package:easy_pasta/model/app_theme.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | 16 | // 添加错误处理 17 | FlutterError.onError = (FlutterErrorDetails details) { 18 | debugPrint('Flutter error: ${details.exception}'); 19 | debugPrint('Stack trace: ${details.stack}'); 20 | }; 21 | 22 | WindowService(); 23 | TrayService(); 24 | HotkeyService(); 25 | StartupService(); 26 | 27 | // runApp(const MyApp()); 28 | runApp(MyTextApp()); 29 | } 30 | 31 | class MyTextApp extends StatelessWidget { 32 | @override 33 | Widget build(BuildContext context) { 34 | return MaterialApp( 35 | title: 'Bonsoir Test', 36 | theme: ThemeData( 37 | primarySwatch: Colors.blue, 38 | ), 39 | home: BonjourTestPage(), 40 | ); 41 | } 42 | } 43 | 44 | class MyApp extends StatelessWidget { 45 | const MyApp({super.key}); 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return MultiProvider( 50 | providers: [ 51 | ChangeNotifierProvider(create: (_) => PboardProvider()), 52 | ChangeNotifierProvider(create: (_) => ThemeProvider()), 53 | ], 54 | child: Consumer( 55 | builder: (context, themeProvider, _) { 56 | return MaterialApp( 57 | debugShowCheckedModeBanner: false, 58 | title: 'Easy Pasta', 59 | theme: AppTheme.light(), 60 | darkTheme: AppTheme.dark(), 61 | themeMode: themeProvider.themeMode, 62 | home: const MyHomePage(), 63 | ); 64 | }, 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | void fl_register_plugins(FlPluginRegistry* registry) { 18 | g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); 20 | hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); 21 | g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); 23 | irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); 24 | g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); 26 | screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); 27 | g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = 28 | fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); 29 | super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); 30 | g_autoptr(FlPluginRegistrar) tray_manager_registrar = 31 | fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); 32 | tray_manager_plugin_register_with_registrar(tray_manager_registrar); 33 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 34 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 35 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 36 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 37 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 38 | window_manager_plugin_register_with_registrar(window_manager_registrar); 39 | } 40 | -------------------------------------------------------------------------------- /lib/widget/cards/app_info_card.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AppInfoContent extends StatelessWidget { 5 | final Uint8List? appIcon; 6 | final String appName; 7 | final double iconSize; 8 | final double fontSize; 9 | final double maxWidth; 10 | final Color? textColor; 11 | final TextStyle? textStyle; 12 | 13 | const AppInfoContent({ 14 | Key? key, 15 | this.appIcon, 16 | required this.appName, 17 | this.iconSize = 14, 18 | this.fontSize = 11, 19 | this.maxWidth = 150, 20 | this.textColor, 21 | this.textStyle, 22 | }) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Row( 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | if (appIcon != null) ...[ 30 | _buildAppIcon(), 31 | const SizedBox(width: 4), 32 | ], 33 | _buildAppName(context), 34 | ], 35 | ); 36 | } 37 | 38 | Widget _buildAppIcon() { 39 | return ClipRRect( 40 | borderRadius: BorderRadius.circular(4), 41 | child: Image.memory( 42 | appIcon!, 43 | width: iconSize, 44 | height: iconSize, 45 | errorBuilder: (context, error, stackTrace) { 46 | return Icon( 47 | Icons.apps, 48 | size: iconSize, 49 | color: Colors.grey[400], 50 | ); 51 | }, 52 | ), 53 | ); 54 | } 55 | 56 | Widget _buildAppName(BuildContext context) { 57 | // 获取默认样式 58 | final defaultStyle = Theme.of(context).textTheme.bodySmall?.copyWith( 59 | color: textColor ?? Colors.grey[600], 60 | fontSize: fontSize, 61 | ); 62 | 63 | return ConstrainedBox( 64 | constraints: BoxConstraints(maxWidth: maxWidth), 65 | child: Tooltip( 66 | message: appName, 67 | child: Text( 68 | appName, 69 | style: textStyle ?? defaultStyle, 70 | maxLines: 1, 71 | overflow: TextOverflow.ellipsis, 72 | softWrap: false, 73 | ), 74 | ), 75 | ); 76 | } 77 | 78 | /// 获取应用图标的Widget 79 | Widget? get appIconWidget { 80 | if (appIcon == null) return null; 81 | return _buildAppIcon(); 82 | } 83 | 84 | /// 检查应用名称是否需要截断 85 | bool get isNameTruncated { 86 | final textPainter = TextPainter( 87 | text: TextSpan(text: appName), 88 | maxLines: 1, 89 | textDirection: TextDirection.ltr, 90 | )..layout(maxWidth: maxWidth); 91 | 92 | return textPainter.didExceedMaxLines; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/widget/cards/text_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | class TextContent extends StatelessWidget { 5 | static final _urlPattern = RegExp( 6 | r'(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})', 7 | caseSensitive: false, 8 | ); 9 | 10 | final String text; 11 | final double fontSize; 12 | final TextStyle? style; 13 | final TextAlign textAlign; 14 | final bool selectable; 15 | 16 | const TextContent({ 17 | super.key, 18 | required this.text, 19 | this.fontSize = 13, 20 | this.style, 21 | this.textAlign = TextAlign.left, 22 | this.selectable = false, 23 | }); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final textStyle = style ?? _defaultTextStyle(context); 28 | return _urlPattern.hasMatch(text) 29 | ? _buildUrlText(textStyle) 30 | : _buildNormalText(textStyle); 31 | } 32 | 33 | TextStyle _defaultTextStyle(BuildContext context) => TextStyle( 34 | fontSize: fontSize, 35 | height: 1.2, 36 | color: Theme.of(context).textTheme.bodyMedium?.color, 37 | ); 38 | 39 | Widget _buildUrlText(TextStyle baseStyle) { 40 | return InkWell( 41 | onTap: () => _launchURL(text), 42 | child: Text( 43 | text.length > 500 ? text.substring(0, 500) : text, 44 | textAlign: textAlign, 45 | softWrap: true, 46 | style: baseStyle.copyWith( 47 | color: Colors.blue, 48 | decoration: TextDecoration.underline, 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | Widget _buildNormalText(TextStyle style) { 55 | return selectable 56 | ? SelectableText( 57 | text, 58 | textAlign: textAlign, 59 | style: style, 60 | ) 61 | : Text( 62 | text, 63 | textAlign: textAlign, 64 | softWrap: true, 65 | style: style, 66 | ); 67 | } 68 | 69 | /// 打开URL 70 | Future _launchURL(String url) async { 71 | final uri = Uri.parse(url); 72 | if (await canLaunchUrl(uri)) { 73 | await launchUrl(uri); 74 | } 75 | } 76 | 77 | /// 获取文本的预览内容 78 | String get previewText { 79 | if (text.length <= 100) return text; 80 | return '${text.substring(0, 97)}...'; 81 | } 82 | 83 | /// 检查文本是否为空 84 | bool get isEmpty => text.trim().isEmpty; 85 | 86 | /// 获取文本的字数统计 87 | int get wordCount => text.trim().length; 88 | } 89 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | ### 项目规范 2 | 3 | - 项目名称:`easy_paste` 4 | - 项目描述:`一个基于flutter的粘贴板跨平台应用,支持 mac、windows、linux` 5 | - 项目目录结构: 6 | - `lib/`:包含应用程序的源代码。 7 | - `test/`:包含测试代码。 8 | - `windows/`:包含 Windows 特定的配置和资源。 9 | - `macos/`:包含 macOS 特定的配置和资源。 10 | - `linux/`:包含 Linux 特定的配置和资源。 11 | - `packages/`:包含第三方依赖包。 12 | 13 | ### 项目需求: 14 | - 监听用户的复制粘贴动作,把用户复制的文本、图片、链接、文件、文件夹等保存到数据库中 15 | - 然后当用户使用快捷键`cmd+shift+c`时,弹出窗口显示数据库中的内容 16 | - 用户点击内容后,把内容复制到系统粘贴板,随后隐藏窗口面板,方便复制到其他的地方 17 | - 支持mac、windows、linux 18 | - 在`windows/`目录下实现监听剪贴板,并把剪贴板的内容保存到数据库中 19 | - 在`macos/`目录下实现监听剪贴板,并把剪贴板的内容保存到数据库中 20 | - 在`linux/`目录下实现监听剪贴板,并把剪贴板的内容保存到数据库中 21 | 22 | ### 用户精通技术栈: 23 | 24 | - Flutter 25 | - Dart 26 | - Swift 27 | - C# 28 | - .NET 29 | - C++ 30 | - C 31 | 32 | ### 关键原则: 33 | 34 | - 编写简洁的、技术性的 Dart 代码并提供准确示例。 35 | - 在适当场合使用函数式编程和声明式编程模式。 36 | - 优先使用组合而非继承。 37 | - 使用描述性变量名和辅助动词(如:isLoading,hasError)。 38 | - 文件结构应包括:导出的 widget、子 widget、助手函数、静态内容、类型。 39 | 40 | ### Dart/Flutter 编码规范: 41 | 42 | - 对于不可变的 widget,使用 `const` 构造函数。 43 | - 使用 Freezed 创建不可变的状态类和联合类型。 44 | - 对于简单的函数和方法,使用箭头语法。 45 | - 对于单行 getter 和 setter 使用表达式体。 46 | - 使用尾随逗号优化格式和 diff。 47 | 48 | ### 错误处理与验证: 49 | 50 | - 在视图中使用 `SelectableText.rich` 进行错误处理,避免使用 SnackBars。 51 | - 使用红色文本在 `SelectableText.rich` 中显示错误。 52 | - 处理空状态并在界面中显示。 53 | - 使用 `AsyncValue` 处理错误和加载状态。 54 | 55 | ### Riverpod 特定的规范: 56 | 57 | - 使用 `@riverpod` 注解生成 provider。 58 | - 优先使用 `AsyncNotifierProvider` 和 `NotifierProvider`,避免使用 `StateProvider`。 59 | - 避免使用 `StateProvider`、`StateNotifierProvider` 和 `ChangeNotifierProvider`。 60 | - 使用 `ref.invalidate()` 手动触发 provider 更新。 61 | - 在 widget 销毁时正确取消异步操作。 62 | 63 | ### 性能优化: 64 | 65 | - 尽可能使用 `const` widget 来优化重建。 66 | - 使用 `ListView.builder` 优化列表视图。 67 | - 对于静态图片使用 `AssetImage`,对于远程图片使用 `cached_network_image`。 68 | - 为 Supabase 操作添加适当的错误处理,包括网络错误。 69 | 70 | ### 关键约定: 71 | 72 | 1. 使用 `GoRouter` 或 `auto_route` 进行导航和深度链接。 73 | 2. 优化 Flutter 性能指标(如首屏绘制时间,互动时间)。 74 | 3. 优先使用无状态 widget: 75 | - 使用 Riverpod 的 `ConsumerWidget` 处理依赖状态的 widget。 76 | - 结合 Riverpod 和 Flutter Hooks 时,使用 `HookConsumerWidget`。 77 | 78 | ### UI 和样式: 79 | 80 | - 使用 Flutter 的内置 widget 并创建自定义 widget。 81 | - 使用 `LayoutBuilder` 或 `MediaQuery` 实现响应式设计。 82 | - 使用主题来保持一致的应用样式。 83 | - 使用 `Theme.of(context).textTheme.titleLarge` 替代 `headline6`,使用 `headlineSmall` 替代 `headline5` 等。 84 | 85 | ### 模型和数据库约定: 86 | 87 | - 数据库表中包含 `createdAt`、`updatedAt` 和 `isDeleted` 字段。 88 | - 使用 `@JsonSerializable(fieldRename: FieldRename.snake)` 为模型进行 JSON 序列化。 89 | - 对只读字段使用 `@JsonKey(includeFromJson: true, includeToJson: false)`。 90 | 91 | ### widget 和 UI 组件: 92 | 93 | - 创建小型的私有 widget 类,而不是像 `Widget _build...` 这样的函数。 94 | - 使用 `RefreshIndicator` 实现下拉刷新功能。 95 | - 在 `TextField` 中设置适当的 `textCapitalization`、`keyboardType` 和 `textInputAction`。 96 | - 使用 `Image.network` 时,始终包含 `errorBuilder`。 97 | 98 | ### 其他事项: 99 | 100 | - 使用 `log` 而非 `print` 进行调试。 101 | - 在适当的地方使用 Flutter Hooks / Riverpod Hooks。 102 | - 保持每行代码不超过 80 个字符,多个参数函数调用时,在闭括号前加逗号。 103 | - 对数据库中的枚举使用 `@JsonValue(int)`。 104 | 105 | ### 代码生成: 106 | 107 | - 发挥自己的设计能力,设计出符合项目需求的UI 108 | 109 | ### 文档: 110 | 111 | - TREE.md 文件中记录项目目录结构 112 | - README.md 文件中记录项目介绍 113 | 114 | ### 参考: 115 | 116 | - 遵循 flutter 官方文档: https://docs.flutter.cn/ 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyPasta 2 | 3 |

4 | EasyPasta Screenshot 5 |

6 |

7 | EasyPasta Screenshot 8 |

9 | 10 |

11 | EasyPasta Screenshot 12 |

13 | 14 |

15 | EasyPasta Screenshot 16 |

17 | 18 | ## 📝 概述 19 | 20 | EasyPasta 是一款强大的跨平台剪贴板管理工具,专为提升您的工作效率而设计。它能够自动记录您的复制历史,并通过简单的快捷键操作随时调用,让信息的复制和粘贴变得更加便捷。 21 | 22 | ## 🛠️ 开发环境 23 | 24 | - Flutter 3.24.4 25 | - Dart 3.5.4 26 | 27 | ## 📦 下载 28 | 29 | [macOS](https://github.com/DargonLee/easy_pasta/releases/download/v2.0.0/EasyPasta-2.0.0.dmg) 30 | 31 | Windows (暂未发布,欢迎贡献编译版本) 32 | 33 | ```shell 34 | scripts\build.bat 35 | ``` 36 | 37 | Linux (暂未发布,欢迎贡献编译版本) 38 | 39 | ```shell 40 | ./scripts/build.sh 41 | ``` 42 | 43 | ### ✨ 核心特性 44 | 45 | - 🔒 **本地存储**: 所有数据均存储在本地,确保您的隐私安全 46 | - 🔍 **智能搜索**: 快速查找历史剪贴板内容 47 | - ⌨️ **快捷键支持**: 自定义快捷键,随时唤起面板 48 | - 🖼️ **多格式支持**: 支持文本、图片、文件等多种格式 49 | - 🚀 **启动项**: 支持开机自启动 50 | - 💪 **跨平台**: 支持 macOS、Windows 和 Linux 51 | 52 | ## 🖥️ 系统要求 53 | 54 | - macOS 10.15 或更高版本 55 | - Windows 10 或更高版本 56 | - Linux (Ubuntu 20.04 或其他主流发行版) 57 | 58 | ## 📥 安装指南 59 | 60 | ### macOS 61 | 62 | 1. 下载最新的 DMG 安装包 63 | 2. 打开 DMG 文件 64 | 3. 将 EasyPasta 拖入 Applications 文件夹 65 | 4. 从 Applications 文件夹启动 EasyPasta 66 | 67 | ### Windows 68 | 69 | 1. 下载最新的 Windows 安装包 70 | 2. 运行安装程序 71 | 3. 按照安装向导完成安装 72 | 73 | ### Linux 74 | 75 | 1. 下载最新的 .deb 包(Ubuntu/Debian) 76 | 2. 运行以下命令安装: 77 | 78 | ```bash 79 | sudo dpkg -i easy_pasta_linux_amd64.deb 80 | ``` 81 | 82 | ## 🎯 使用方法 83 | 84 | 1. **启动应用** 85 | 86 | - 启动后,状态栏会显示 EasyPasta 图标 87 | 88 |

89 | Status Bar Icon 90 |

91 | 92 | 2. **访问剪贴板历史** 93 | 94 | - 点击状态栏图标 95 | - 或使用默认快捷键 `Cmd+Shift+V` (macOS) / `Ctrl+Shift+V` (Windows/Linux) 96 | 97 | 3. **使用剪贴板内容** 98 | 99 | - 点击复制图标 100 | - 双击复制到系统剪贴板 101 | - 在目标位置粘贴 102 | 103 | 4. **剪贴板操作** 104 | 105 | - 点击复制按钮,复制内容到系统剪贴板 106 | - 点击收藏按钮,将内容添加到收藏列表 107 | - 点击删除按钮,删除选中的内容 108 | 109 | 5. **预览** 110 | 111 | - 鼠标放在卡片上,按空格键预览 112 | 113 | 6. **关闭窗口** 114 | 115 | - 使用快捷键 `Cmd+W` (macOS) / `Ctrl+W` (Windows/Linux) 116 | 117 | 7. **退出应用** 118 | 119 | - 使用快捷键 `Cmd+Q` (macOS) / `Ctrl+Q` (Windows/Linux) 120 | 121 | ## ⚙️ 配置选项 122 | 123 | - **快捷键设置**: 自定义唤起快捷键 124 | - **启动选项**: 设置开机自启动 125 | - **历史记录**: 配置历史记录保存数量 126 | 127 | ## 📄 许可证 128 | 129 | 本项目基于 MIT 许可证开源 - 查看 [LICENSE](LICENSE) 文件了解更多详情 130 | 131 | ## ☕️ 支持项目 132 | 133 | 如果您觉得这个项目对您有帮助,欢迎请我喝杯咖啡 :) 134 | 135 |
136 | Buy Me A Coffee 137 |
138 | 139 |
140 | 141 |
142 | 143 |

144 | Made with ❤️ by harlans 145 |

146 | -------------------------------------------------------------------------------- /lib/core/record_hotkey_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hotkey_manager/hotkey_manager.dart'; 3 | 4 | class RecordHotKeyDialog extends StatefulWidget { 5 | const RecordHotKeyDialog({ 6 | super.key, 7 | required this.onHotKeyRecorded, 8 | }); 9 | 10 | final ValueChanged onHotKeyRecorded; 11 | 12 | @override 13 | State createState() => _RecordHotKeyDialogState(); 14 | } 15 | 16 | class _RecordHotKeyDialogState extends State { 17 | HotKey? _hotKey; 18 | 19 | void _handleSetAsInappWideChanged(bool newValue) { 20 | if (_hotKey == null) { 21 | return; 22 | } 23 | _hotKey = HotKey( 24 | key: _hotKey!.key, 25 | modifiers: _hotKey?.modifiers, 26 | scope: newValue ? HotKeyScope.inapp : HotKeyScope.system, 27 | ); 28 | setState(() {}); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return AlertDialog( 34 | content: SingleChildScrollView( 35 | child: ListBody( 36 | children: [ 37 | const Text('The `HotKeyRecorder` widget will record your hotkey.'), 38 | Container( 39 | width: 100, 40 | height: 60, 41 | margin: const EdgeInsets.only(top: 20), 42 | decoration: BoxDecoration( 43 | border: Border.all( 44 | color: Theme.of(context).primaryColor, 45 | ), 46 | ), 47 | child: Stack( 48 | alignment: Alignment.center, 49 | children: [ 50 | HotKeyRecorder( 51 | onHotKeyRecorded: (hotKey) { 52 | _hotKey = hotKey; 53 | setState(() {}); 54 | }, 55 | ), 56 | ], 57 | ), 58 | ), 59 | GestureDetector( 60 | onTap: () { 61 | _handleSetAsInappWideChanged( 62 | _hotKey?.scope != HotKeyScope.inapp, 63 | ); 64 | }, 65 | child: Row( 66 | children: [ 67 | Checkbox( 68 | value: _hotKey?.scope == HotKeyScope.inapp, 69 | onChanged: (newValue) { 70 | _handleSetAsInappWideChanged(newValue!); 71 | }, 72 | ), 73 | const Text( 74 | 'Set as inapp-wide hotkey. (default is system-wide)', 75 | ), 76 | ], 77 | ), 78 | ), 79 | ], 80 | ), 81 | ), 82 | actions: [ 83 | TextButton( 84 | child: const Text('Cancel'), 85 | onPressed: () { 86 | Navigator.of(context).pop(); 87 | }, 88 | ), 89 | TextButton( 90 | onPressed: _hotKey == null 91 | ? null 92 | : () { 93 | widget.onHotKeyRecorded(_hotKey!); 94 | Navigator.of(context).pop(); 95 | }, 96 | child: const Text('OK'), 97 | ), 98 | ], 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/providers/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/db/shared_preference_helper.dart'; 3 | 4 | /// 主题管理器 5 | /// 负责管理应用的主题模式,并持久化保存用户的主题偏好 6 | class ThemeProvider extends ChangeNotifier { 7 | // 私有成员 8 | late final SharedPreferenceHelper _prefs; 9 | late ThemeMode _themeMode = ThemeMode.system; 10 | 11 | // 构造函数 12 | ThemeProvider() { 13 | _initPrefs(); 14 | } 15 | 16 | /// 初始化偏好设置 17 | Future _initPrefs() async { 18 | _prefs = await SharedPreferenceHelper.instance; 19 | _initTheme(); 20 | notifyListeners(); 21 | } 22 | 23 | /// 初始化主题 24 | void _initTheme() { 25 | final savedThemeMode = _prefs.getThemeMode(); 26 | _themeMode = ThemeMode.values[savedThemeMode]; 27 | } 28 | 29 | /// 获取当前主题模式 30 | ThemeMode get themeMode => _themeMode; 31 | 32 | /// 判断是否为暗黑模式 33 | bool get isDarkMode => _themeMode == ThemeMode.dark; 34 | 35 | /// 判断是否跟随系统 36 | bool get isSystemMode => _themeMode == ThemeMode.system; 37 | 38 | /// 设置主题模式 39 | Future setThemeMode(ThemeMode mode) async { 40 | if (_themeMode == mode) return; 41 | 42 | try { 43 | await _prefs.setThemeMode(mode.index); 44 | _themeMode = mode; 45 | notifyListeners(); 46 | } catch (e) { 47 | debugPrint('设置主题失败: $e'); 48 | // 可以在这里添加错误处理逻辑 49 | } 50 | } 51 | 52 | /// 切换明暗主题 53 | Future toggleTheme() async { 54 | final newMode = 55 | _themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark; 56 | await setThemeMode(newMode); 57 | } 58 | 59 | /// 切换是否跟随系统 60 | Future toggleSystemMode() async { 61 | final newMode = _themeMode == ThemeMode.system 62 | ? (isDarkMode ? ThemeMode.dark : ThemeMode.light) 63 | : ThemeMode.system; 64 | await setThemeMode(newMode); 65 | } 66 | 67 | /// 获取主题相关文本 68 | String getThemeModeText() { 69 | switch (_themeMode) { 70 | case ThemeMode.system: 71 | return '跟随系统'; 72 | case ThemeMode.light: 73 | return '浅色模式'; 74 | case ThemeMode.dark: 75 | return '深色模式'; 76 | } 77 | } 78 | 79 | /// 获取主题图标 80 | IconData getThemeModeIcon() { 81 | switch (_themeMode) { 82 | case ThemeMode.system: 83 | return Icons.brightness_auto; 84 | case ThemeMode.light: 85 | return Icons.brightness_7; 86 | case ThemeMode.dark: 87 | return Icons.brightness_4; 88 | } 89 | } 90 | 91 | /// 获取当前主题的颜色方案 92 | ColorScheme getColorScheme(BuildContext context) { 93 | final brightness = _themeMode == ThemeMode.system 94 | ? MediaQuery.platformBrightnessOf(context) 95 | : _themeMode == ThemeMode.dark 96 | ? Brightness.dark 97 | : Brightness.light; 98 | 99 | return brightness == Brightness.dark 100 | ? const ColorScheme.dark( 101 | primary: Colors.blue, 102 | secondary: Colors.blueAccent, 103 | surface: Color(0xFF1E1E1E), 104 | ) 105 | : const ColorScheme.light( 106 | primary: Colors.blue, 107 | secondary: Colors.blueAccent, 108 | surface: Colors.white, 109 | ); 110 | } 111 | 112 | /// 释放资源 113 | @override 114 | void dispose() { 115 | super.dispose(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "easy_pasta" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "easy_pasta" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "easy_pasta.exe" "\0" 98 | VALUE "ProductName", "easy_pasta" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /lib/model/pasteboard_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:uuid/uuid.dart'; 4 | import 'package:easy_pasta/model/clipboard_type.dart'; 5 | 6 | /// 剪贴板数据模型 7 | class ClipboardItemModel { 8 | // 基础属性 9 | final String id; 10 | final String time; 11 | final ClipboardType? ptype; 12 | final String pvalue; 13 | bool isFavorite; 14 | final Uint8List? bytes; 15 | 16 | /// 创建剪贴板数据模型 17 | ClipboardItemModel({ 18 | String? id, 19 | String? time, 20 | required this.ptype, 21 | required this.pvalue, 22 | this.isFavorite = false, 23 | this.bytes, 24 | }) : id = id ?? const Uuid().v4(), 25 | time = time ?? DateTime.now().toString(); 26 | 27 | /// 从数据库映射创建模型 28 | factory ClipboardItemModel.fromMapObject(Map map) { 29 | return ClipboardItemModel( 30 | id: map['id'], 31 | time: map['time'], 32 | ptype: ClipboardType.fromString(map['type']), 33 | pvalue: map['value'], 34 | isFavorite: map['isFavorite'] == 1, 35 | bytes: map['bytes'], 36 | ); 37 | } 38 | 39 | /// 转换为数据库映射 40 | Map toMap() { 41 | return { 42 | 'id': id, 43 | 'time': time, 44 | 'type': ptype.toString(), 45 | 'value': pvalue, 46 | 'isFavorite': isFavorite ? 1 : 0, 47 | 'bytes': bytes, 48 | }; 49 | } 50 | 51 | /// 获取HTML数据 52 | String? get htmlData => 53 | ptype == ClipboardType.html ? bytesToString(bytes ?? Uint8List(0)) : null; 54 | 55 | /// 获取图片数据 56 | Uint8List? get imageBytes => ptype == ClipboardType.image ? bytes : null; 57 | 58 | /// 获取文件路径 59 | String? get filePath => 60 | ptype == ClipboardType.file ? bytesToString(bytes ?? Uint8List(0)) : null; 61 | 62 | /// 将字符串转换为Uint8List 63 | static Uint8List stringToBytes(String str) { 64 | return Uint8List.fromList(utf8.encode(str)); 65 | } 66 | 67 | /// 将Uint8List转换为字符串 68 | String bytesToString(Uint8List bytes) { 69 | return utf8.decode(bytes); 70 | } 71 | 72 | /// 复制模型并更新部分属性 73 | ClipboardItemModel copyWith({ 74 | String? id, 75 | String? time, 76 | ClipboardType? ptype, 77 | String? pvalue, 78 | bool? isFavorite, 79 | Uint8List? bytes, 80 | }) { 81 | return ClipboardItemModel( 82 | id: id ?? this.id, 83 | time: time ?? this.time, 84 | ptype: ptype ?? this.ptype, 85 | pvalue: pvalue ?? this.pvalue, 86 | isFavorite: isFavorite ?? this.isFavorite, 87 | bytes: bytes ?? this.bytes, 88 | ); 89 | } 90 | 91 | /// 切换收藏状态 92 | ClipboardItemModel toggleFavorite() { 93 | return copyWith(isFavorite: !isFavorite); 94 | } 95 | 96 | @override 97 | bool operator ==(Object other) { 98 | if (identical(this, other)) return true; 99 | if (other is! ClipboardItemModel) return false; 100 | 101 | switch (ptype) { 102 | case ClipboardType.text: 103 | return pvalue == other.pvalue && ptype == other.ptype; 104 | case ClipboardType.html: 105 | return pvalue == other.pvalue && 106 | ptype == other.ptype && 107 | listEquals(bytes, other.bytes); 108 | case ClipboardType.file: 109 | return pvalue == other.pvalue && ptype == other.ptype; 110 | case ClipboardType.image: 111 | return ptype == other.ptype && listEquals(bytes, other.bytes); 112 | default: 113 | return false; 114 | } 115 | } 116 | 117 | @override 118 | int get hashCode => Object.hash(id, time, ptype, pvalue, bytes, isFavorite); 119 | 120 | @override 121 | String toString() => 122 | 'ClipboardItemModel(id: $id, time: $time, type: $ptype, value: $pvalue, isFavorite: $isFavorite)'; 123 | } 124 | -------------------------------------------------------------------------------- /lib/widget/cards/footer_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/model/pasteboard_model.dart'; 3 | import 'package:easy_pasta/core/icon_service.dart'; 4 | import 'package:easy_pasta/model/clipboard_type.dart'; 5 | 6 | class FooterContent extends StatelessWidget { 7 | final ClipboardItemModel model; 8 | final Function(ClipboardItemModel) onCopy; 9 | final Function(ClipboardItemModel) onFavorite; 10 | final Function(ClipboardItemModel) onDelete; 11 | 12 | const FooterContent({ 13 | Key? key, 14 | required this.model, 15 | required this.onCopy, 16 | required this.onFavorite, 17 | required this.onDelete, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final defaultStyle = Theme.of(context).textTheme.bodySmall?.copyWith( 23 | color: Colors.grey[500], 24 | fontSize: 10, 25 | ); 26 | 27 | return Padding( 28 | padding: const EdgeInsets.only(top: 4), 29 | child: Row( 30 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 31 | children: [ 32 | Icon( 33 | TypeIconHelper.getTypeIcon(model.ptype ?? ClipboardType.unknown, 34 | pvalue: model.pvalue), 35 | size: 15, 36 | color: Theme.of(context).colorScheme.primary, 37 | ), 38 | const SizedBox(width: 4), 39 | Text( 40 | _formatTimestamp(DateTime.parse(model.time)), 41 | style: defaultStyle, 42 | ), 43 | const Spacer(), 44 | IconButton( 45 | icon: const Icon(Icons.copy, size: 14), 46 | onPressed: () => onCopy(model), 47 | padding: EdgeInsets.zero, 48 | constraints: const BoxConstraints(), 49 | splashRadius: 12, 50 | color: Colors.grey[500], 51 | ), 52 | const SizedBox(width: 8), 53 | IconButton( 54 | icon: Icon( 55 | model.isFavorite ? Icons.star : Icons.star_border, 56 | size: 15, 57 | ), 58 | onPressed: () => onFavorite(model), 59 | padding: EdgeInsets.zero, 60 | constraints: const BoxConstraints(), 61 | splashRadius: 12, 62 | color: model.isFavorite ? Colors.amber : Colors.grey[500], 63 | ), 64 | const SizedBox(width: 8), 65 | IconButton( 66 | icon: const Icon(Icons.delete_outline, size: 15), 67 | onPressed: () => onDelete(model), 68 | padding: EdgeInsets.zero, 69 | constraints: const BoxConstraints(), 70 | splashRadius: 12, 71 | color: Colors.grey[500], 72 | ), 73 | const SizedBox(width: 8), 74 | ], 75 | ), 76 | ); 77 | } 78 | 79 | String _formatTimestamp(DateTime timestamp) { 80 | final now = DateTime.now(); 81 | final difference = now.difference(timestamp); 82 | 83 | if (difference.inMinutes < 1) { 84 | return '刚刚'; 85 | } else if (difference.inHours < 1) { 86 | return '${difference.inMinutes}分钟前'; 87 | } else if (difference.inDays < 1) { 88 | return '${difference.inHours}小时前'; 89 | } else if (difference.inDays < 30) { 90 | return '${difference.inDays}天前'; 91 | } else { 92 | return '${timestamp.month}月${timestamp.day}日'; 93 | } 94 | } 95 | 96 | String getDetailedTime(DateTime timestamp) { 97 | return '${timestamp.year}年${timestamp.month}月${timestamp.day}日 ' 98 | '${timestamp.hour.toString().padLeft(2, '0')}:' 99 | '${timestamp.minute.toString().padLeft(2, '0')}'; 100 | } 101 | 102 | bool isToday(DateTime timestamp) { 103 | final now = DateTime.now(); 104 | return timestamp.year == now.year && 105 | timestamp.month == now.month && 106 | timestamp.day == now.day; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # === Flutter Library === 14 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 15 | 16 | # Published to parent scope for install step. 17 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 18 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 19 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 20 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 21 | 22 | list(APPEND FLUTTER_LIBRARY_HEADERS 23 | "flutter_export.h" 24 | "flutter_windows.h" 25 | "flutter_messenger.h" 26 | "flutter_plugin_registrar.h" 27 | "flutter_texture_registrar.h" 28 | ) 29 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 30 | add_library(flutter INTERFACE) 31 | target_include_directories(flutter INTERFACE 32 | "${EPHEMERAL_DIR}" 33 | ) 34 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 35 | add_dependencies(flutter flutter_assemble) 36 | 37 | # === Wrapper === 38 | list(APPEND CPP_WRAPPER_SOURCES_CORE 39 | "core_implementations.cc" 40 | "standard_codec.cc" 41 | ) 42 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 43 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 44 | "plugin_registrar.cc" 45 | ) 46 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 47 | list(APPEND CPP_WRAPPER_SOURCES_APP 48 | "flutter_engine.cc" 49 | "flutter_view_controller.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 52 | 53 | # Wrapper sources needed for a plugin. 54 | add_library(flutter_wrapper_plugin STATIC 55 | ${CPP_WRAPPER_SOURCES_CORE} 56 | ${CPP_WRAPPER_SOURCES_PLUGIN} 57 | ) 58 | apply_standard_settings(flutter_wrapper_plugin) 59 | set_target_properties(flutter_wrapper_plugin PROPERTIES 60 | POSITION_INDEPENDENT_CODE ON) 61 | set_target_properties(flutter_wrapper_plugin PROPERTIES 62 | CXX_VISIBILITY_PRESET hidden) 63 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 64 | target_include_directories(flutter_wrapper_plugin PUBLIC 65 | "${WRAPPER_ROOT}/include" 66 | ) 67 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 68 | 69 | # Wrapper sources needed for the runner. 70 | add_library(flutter_wrapper_app STATIC 71 | ${CPP_WRAPPER_SOURCES_CORE} 72 | ${CPP_WRAPPER_SOURCES_APP} 73 | ) 74 | apply_standard_settings(flutter_wrapper_app) 75 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 76 | target_include_directories(flutter_wrapper_app PUBLIC 77 | "${WRAPPER_ROOT}/include" 78 | ) 79 | add_dependencies(flutter_wrapper_app flutter_assemble) 80 | 81 | # === Flutter tool backend === 82 | # _phony_ is a non-existent file to force this command to run every time, 83 | # since currently there's no way to get a full input/output list from the 84 | # flutter tool. 85 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 86 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 87 | add_custom_command( 88 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 89 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 90 | ${CPP_WRAPPER_SOURCES_APP} 91 | ${PHONY_OUTPUT} 92 | COMMAND ${CMAKE_COMMAND} -E env 93 | ${FLUTTER_TOOL_ENVIRONMENT} 94 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 95 | windows-x64 $ 96 | VERBATIM 97 | ) 98 | add_custom_target(flutter_assemble DEPENDS 99 | "${FLUTTER_LIBRARY}" 100 | ${FLUTTER_LIBRARY_HEADERS} 101 | ${CPP_WRAPPER_SOURCES_CORE} 102 | ${CPP_WRAPPER_SOURCES_PLUGIN} 103 | ${CPP_WRAPPER_SOURCES_APP} 104 | ) 105 | -------------------------------------------------------------------------------- /lib/page/pboard_card_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:typed_data'; 3 | import 'package:easy_pasta/model/pasteboard_model.dart'; 4 | import 'package:easy_pasta/widget/cards/image_card.dart'; 5 | import 'package:easy_pasta/widget/cards/file_card.dart'; 6 | import 'package:easy_pasta/widget/cards/text_card.dart'; 7 | import 'package:easy_pasta/widget/cards/footer_card.dart'; 8 | import 'package:easy_pasta/widget/cards/html_card.dart'; 9 | import 'package:easy_pasta/model/clipboard_type.dart'; 10 | 11 | class NewPboardItemCard extends StatelessWidget { 12 | final ClipboardItemModel model; 13 | final String selectedId; 14 | final Function(ClipboardItemModel) onTap; 15 | final Function(ClipboardItemModel) onDoubleTap; 16 | final Function(ClipboardItemModel) onCopy; 17 | final Function(ClipboardItemModel) onFavorite; 18 | final Function(ClipboardItemModel) onDelete; 19 | const NewPboardItemCard({ 20 | Key? key, 21 | required this.model, 22 | required this.selectedId, 23 | required this.onTap, 24 | required this.onDoubleTap, 25 | required this.onCopy, 26 | required this.onFavorite, 27 | required this.onDelete, 28 | }) : super(key: key); 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | final isSelected = selectedId == model.id; 33 | return RepaintBoundary( 34 | child: Card( 35 | elevation: isSelected ? 2 : 0, 36 | margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 37 | shape: RoundedRectangleBorder( 38 | borderRadius: BorderRadius.circular(12), 39 | side: isSelected 40 | ? const BorderSide(color: Colors.blue, width: 1) 41 | : BorderSide.none, 42 | ), 43 | child: InkWell( 44 | borderRadius: BorderRadius.circular(12), 45 | onTap: () => onTap(model), 46 | onDoubleTap: () => onDoubleTap(model), 47 | child: LayoutBuilder(builder: (context, constraints) { 48 | return Padding( 49 | padding: const EdgeInsets.all(8), 50 | child: Column( 51 | crossAxisAlignment: CrossAxisAlignment.stretch, 52 | mainAxisSize: MainAxisSize.min, 53 | children: [ 54 | ConstrainedBox( 55 | constraints: BoxConstraints( 56 | minHeight: 50, 57 | maxHeight: constraints.maxHeight - 38, 58 | ), 59 | child: _buildContent(context), 60 | ), 61 | const Spacer(), 62 | _buildFooter(context), 63 | ], 64 | ), 65 | ); 66 | }), 67 | ), 68 | ), 69 | ); 70 | } 71 | 72 | Widget _buildContent(BuildContext context) { 73 | return Container( 74 | padding: const EdgeInsets.symmetric(vertical: 4), 75 | child: _buildContentByType(context), 76 | ); 77 | } 78 | 79 | Widget _buildContentByType(BuildContext context) { 80 | switch (model.ptype) { 81 | case ClipboardType.image: 82 | return ImageContent( 83 | imageBytes: model.bytes ?? Uint8List(0), 84 | ); 85 | case ClipboardType.file: 86 | return FileContent( 87 | fileName: model.pvalue, 88 | fileUri: model.bytesToString(model.bytes ?? Uint8List(0)), 89 | ); 90 | case ClipboardType.html: 91 | return HtmlContent( 92 | htmlData: model.bytesToString(model.bytes ?? Uint8List(0)), 93 | ); 94 | case ClipboardType.unknown: 95 | return const TextContent( 96 | text: 'Unknown', 97 | ); 98 | default: 99 | return TextContent( 100 | text: model.pvalue, 101 | ); 102 | } 103 | } 104 | 105 | Widget _buildFooter(BuildContext context) { 106 | return FooterContent( 107 | model: model, 108 | onCopy: onCopy, 109 | onFavorite: onFavorite, 110 | onDelete: onDelete, 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/db/shared_preference_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'dart:io' show Platform; 4 | 5 | /// 持久化存储帮助类 6 | /// 负责管理应用程序的配置项存储 7 | class SharedPreferenceHelper { 8 | // 私有构造函数 9 | SharedPreferenceHelper._(); 10 | 11 | // 静态实例 12 | static SharedPreferenceHelper? _instance; 13 | 14 | // SharedPreferences 实例 15 | static SharedPreferences? _preferences; 16 | 17 | /// 存储键名常量 18 | static const String _keyPrefix = 'Pboard_'; 19 | static const String shortcutKey = '${_keyPrefix}ShortcutKey'; 20 | static const String loginInLaunchKey = '${_keyPrefix}LoginInLaunchKey'; 21 | static const String maxItemStoreKey = '${_keyPrefix}MaxItemStoreKey'; 22 | static const String themeModeKey = '${_keyPrefix}ThemeModeKey'; 23 | /// 默认值常量 24 | static const int defaultMaxItems = 50; 25 | static const bool defaultLoginInLaunch = false; 26 | 27 | /// 平台特定的默认快捷键 28 | static String get defaultShortcut { 29 | if (Platform.isMacOS) { 30 | return '{"identifier":"ae9b502e-d9c2-4c8c-acf6-100270b8234a","key":{"usageCode":458758},"modifiers":["meta","shift"],"scope":"system"}'; // macOS 默认快捷键 31 | } else if (Platform.isWindows) { 32 | return '{"identifier":"ae9b502e-d9c2-4c8c-acf6-100270b8234a","key":{"usageCode":458758},"modifiers":["control","shift"],"scope":"system"}'; // Windows 默认快捷键 33 | } else if (Platform.isLinux) { 34 | return '{"identifier":"ae9b502e-d9c2-4c8c-acf6-100270b8234a","key":{"usageCode":458758},"modifiers":["control","shift"],"scope":"system"}'; // Linux 默认快捷键 35 | } else { 36 | return ''; // 其他平台默认为空 37 | } 38 | } 39 | 40 | /// 获取单例实例 41 | static Future get instance async { 42 | _instance ??= SharedPreferenceHelper._(); 43 | _preferences ??= await SharedPreferences.getInstance(); 44 | if (_preferences?.getString(shortcutKey) == null) { 45 | _instance!.setShortcutKey(defaultShortcut); 46 | } 47 | if (_preferences?.getInt(maxItemStoreKey) == null) { 48 | _instance!.setMaxItemStore(defaultMaxItems); 49 | } 50 | return _instance!; 51 | } 52 | 53 | /// 快捷键相关操作 54 | Future setShortcutKey(String value) async { 55 | await _preferences?.setString(shortcutKey, value); 56 | } 57 | 58 | String getShortcutKey() { 59 | return _preferences?.getString(shortcutKey) ?? ''; 60 | } 61 | 62 | /// 最大存储数量相关操作 63 | Future setMaxItemStore(int count) async { 64 | if (count < 0) return; 65 | await _preferences?.setInt(maxItemStoreKey, count); 66 | } 67 | 68 | int getMaxItemStore() { 69 | return _preferences?.getInt(maxItemStoreKey) ?? defaultMaxItems; 70 | } 71 | 72 | /// 开机启动相关操作 73 | Future setLoginInLaunch(bool status) async { 74 | await _preferences?.setBool(loginInLaunchKey, status); 75 | } 76 | 77 | bool getLoginInLaunch() { 78 | return _preferences?.getBool(loginInLaunchKey) ?? defaultLoginInLaunch; 79 | } 80 | 81 | /// 主题模式相关操作 82 | Future setThemeMode(int mode) async { 83 | await _preferences?.setInt(themeModeKey, mode); 84 | } 85 | 86 | int getThemeMode() { 87 | return _preferences?.getInt(themeModeKey) ?? ThemeMode.system.index; 88 | } 89 | 90 | /// 批量操作方法 91 | Future> getAllSettings() async { 92 | return { 93 | 'shortcutKey': getShortcutKey(), 94 | 'maxItemStore': getMaxItemStore(), 95 | 'loginInLaunch': getLoginInLaunch(), 96 | }; 97 | } 98 | 99 | /// 重置所有设置为默认值 100 | Future resetToDefaults() async { 101 | await Future.wait([ 102 | setShortcutKey(defaultShortcut), 103 | setMaxItemStore(defaultMaxItems), 104 | setLoginInLaunch(defaultLoginInLaunch), 105 | ]); 106 | } 107 | 108 | /// 清除特定键的值 109 | Future remove(String key) async { 110 | await _preferences?.remove(key); 111 | } 112 | 113 | /// 清除所有存储的值 114 | Future clearAll() async { 115 | await _preferences?.clear(); 116 | } 117 | 118 | /// 检查键是否存在 119 | bool hasKey(String key) { 120 | return _preferences?.containsKey(key) ?? false; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /lib/widget/preview_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/model/pasteboard_model.dart'; 3 | import 'package:easy_pasta/model/clipboard_type.dart'; 4 | 5 | class PreviewDialog extends StatelessWidget { 6 | final ClipboardItemModel model; 7 | 8 | const PreviewDialog({ 9 | super.key, 10 | required this.model, 11 | }); 12 | 13 | static Future show(BuildContext context, ClipboardItemModel model) { 14 | return showDialog( 15 | context: context, 16 | builder: (context) => PreviewDialog(model: model), 17 | ); 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final theme = Theme.of(context); 23 | final isDark = theme.brightness == Brightness.dark; 24 | 25 | return Dialog( 26 | backgroundColor: isDark ? Colors.grey[900] : Colors.white, 27 | shape: RoundedRectangleBorder( 28 | borderRadius: BorderRadius.circular(12), 29 | ), 30 | child: ConstrainedBox( 31 | constraints: BoxConstraints( 32 | maxWidth: model.ptype == ClipboardType.image ? 800 : 600, 33 | maxHeight: model.ptype == ClipboardType.image ? 600 : 400, 34 | ), 35 | child: Column( 36 | mainAxisSize: MainAxisSize.min, 37 | crossAxisAlignment: CrossAxisAlignment.stretch, 38 | children: [ 39 | _buildHeader(context, isDark), 40 | _buildContentArea(context), 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | 47 | /// 构建标题栏 48 | Widget _buildHeader(BuildContext context, bool isDark) { 49 | final theme = Theme.of(context); 50 | 51 | return Container( 52 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 53 | decoration: BoxDecoration( 54 | border: Border( 55 | bottom: BorderSide( 56 | color: isDark ? Colors.grey[800]! : Colors.grey[200]!, 57 | width: 1, 58 | ), 59 | ), 60 | ), 61 | child: Row( 62 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 63 | children: [ 64 | Text( 65 | '预览', 66 | style: theme.textTheme.titleMedium?.copyWith( 67 | fontWeight: FontWeight.w500, 68 | ), 69 | ), 70 | IconButton( 71 | icon: Icon( 72 | Icons.close, 73 | size: 20, 74 | color: isDark ? Colors.white70 : Colors.black54, 75 | ), 76 | onPressed: () => Navigator.of(context).pop(), 77 | padding: EdgeInsets.zero, 78 | constraints: const BoxConstraints( 79 | minWidth: 32, 80 | minHeight: 32, 81 | ), 82 | style: IconButton.styleFrom( 83 | shape: RoundedRectangleBorder( 84 | borderRadius: BorderRadius.circular(6), 85 | ), 86 | ), 87 | ), 88 | ], 89 | ), 90 | ); 91 | } 92 | 93 | /// 构建内容区域 94 | Widget _buildContentArea(BuildContext context) { 95 | return Expanded( 96 | child: SingleChildScrollView( 97 | physics: const AlwaysScrollableScrollPhysics( 98 | parent: BouncingScrollPhysics(), 99 | ), 100 | child: Padding( 101 | padding: const EdgeInsets.all(16.0), 102 | child: _buildContent(context), 103 | ), 104 | ), 105 | ); 106 | } 107 | 108 | /// 构建内容根据剪贴板类型动态渲染 109 | Widget _buildContent(BuildContext context) { 110 | final theme = Theme.of(context); 111 | 112 | switch (model.ptype) { 113 | case ClipboardType.image: 114 | return Center( 115 | child: Image.memory( 116 | model.imageBytes!, 117 | fit: BoxFit.contain, 118 | ), 119 | ); 120 | case ClipboardType.html: 121 | case ClipboardType.text: 122 | case ClipboardType.file: 123 | default: 124 | return SelectableText( 125 | model.pvalue, 126 | style: theme.textTheme.bodyMedium?.copyWith( 127 | height: 1.5, 128 | letterSpacing: 0.3, 129 | ), 130 | ); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(easy_pasta LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "easy_pasta") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(SET CMP0063 NEW) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Fully re-copy the assets directory on each build to avoid having stale files 91 | # from a previous install. 92 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 93 | install(CODE " 94 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 95 | " COMPONENT Runtime) 96 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 97 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 98 | 99 | # Install the AOT library on non-Debug builds only. 100 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 101 | CONFIGURATIONS Profile;Release 102 | COMPONENT Runtime) 103 | -------------------------------------------------------------------------------- /linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #include 5 | 6 | #ifdef GDK_WINDOWING_X11 7 | #include 8 | #endif 9 | 10 | #include "flutter/generated_plugin_registrant.h" 11 | 12 | struct _MyApplication { 13 | GtkApplication parent_instance; 14 | char** dart_entrypoint_arguments; 15 | }; 16 | 17 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 18 | 19 | // Implements GApplication::activate. 20 | static void my_application_activate(GApplication* application) { 21 | MyApplication* self = MY_APPLICATION(application); 22 | GtkWindow* window = 23 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 24 | 25 | // Use a header bar when running in GNOME as this is the common style used 26 | // by applications and is the setup most users will be using (e.g. Ubuntu 27 | // desktop). 28 | // If running on X and not using GNOME then just use a traditional title bar 29 | // in case the window manager does more exotic layout, e.g. tiling. 30 | // If running on Wayland assume the header bar will work (may need changing 31 | // if future cases occur). 32 | gboolean use_header_bar = TRUE; 33 | #ifdef GDK_WINDOWING_X11 34 | GdkScreen* screen = gtk_window_get_screen(window); 35 | if (GDK_IS_X11_SCREEN(screen)) { 36 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 37 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 38 | use_header_bar = FALSE; 39 | } 40 | } 41 | #endif 42 | if (use_header_bar) { 43 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 44 | gtk_widget_show(GTK_WIDGET(header_bar)); 45 | gtk_header_bar_set_title(header_bar, "easy_pasta"); 46 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 47 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 48 | } else { 49 | gtk_window_set_title(window, "easy_pasta"); 50 | } 51 | 52 | auto bdw = bitsdojo_window_from(window); 53 | bdw->setCustomFrame(true); 54 | //gtk_window_set_default_size(window, 1280, 720); 55 | gtk_widget_show(GTK_WIDGET(window)); 56 | 57 | 58 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 59 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 60 | 61 | FlView* view = fl_view_new(project); 62 | gtk_widget_show(GTK_WIDGET(view)); 63 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 64 | 65 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 66 | 67 | gtk_widget_grab_focus(GTK_WIDGET(view)); 68 | } 69 | 70 | // Implements GApplication::local_command_line. 71 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 72 | MyApplication* self = MY_APPLICATION(application); 73 | // Strip out the first argument as it is the binary name. 74 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 75 | 76 | g_autoptr(GError) error = nullptr; 77 | if (!g_application_register(application, nullptr, &error)) { 78 | g_warning("Failed to register: %s", error->message); 79 | *exit_status = 1; 80 | return TRUE; 81 | } 82 | 83 | g_application_activate(application); 84 | *exit_status = 0; 85 | 86 | return TRUE; 87 | } 88 | 89 | // Implements GObject::dispose. 90 | static void my_application_dispose(GObject* object) { 91 | MyApplication* self = MY_APPLICATION(object); 92 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 93 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 94 | } 95 | 96 | static void my_application_class_init(MyApplicationClass* klass) { 97 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 98 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 99 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 100 | } 101 | 102 | static void my_application_init(MyApplication* self) {} 103 | 104 | MyApplication* my_application_new() { 105 | return MY_APPLICATION(g_object_new(my_application_get_type(), 106 | "application-id", APPLICATION_ID, 107 | "flags", G_APPLICATION_NON_UNIQUE, 108 | nullptr)); 109 | } 110 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - bonsoir_darwin (0.0.1): 3 | - Flutter 4 | - FlutterMacOS 5 | - device_info_plus (0.0.1): 6 | - FlutterMacOS 7 | - FlutterMacOS (1.0.0) 8 | - HotKey (0.2.0) 9 | - hotkey_manager_macos (0.0.1): 10 | - FlutterMacOS 11 | - HotKey 12 | - irondash_engine_context (0.0.1): 13 | - FlutterMacOS 14 | - package_info_plus (0.0.1): 15 | - FlutterMacOS 16 | - path_provider_foundation (0.0.1): 17 | - Flutter 18 | - FlutterMacOS 19 | - screen_retriever_macos (0.0.1): 20 | - FlutterMacOS 21 | - shared_preferences_foundation (0.0.1): 22 | - Flutter 23 | - FlutterMacOS 24 | - super_native_extensions (0.0.1): 25 | - FlutterMacOS 26 | - tray_manager (0.0.1): 27 | - FlutterMacOS 28 | - url_launcher_macos (0.0.1): 29 | - FlutterMacOS 30 | - window_manager (0.2.0): 31 | - FlutterMacOS 32 | 33 | DEPENDENCIES: 34 | - bonsoir_darwin (from `Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin`) 35 | - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) 36 | - FlutterMacOS (from `Flutter/ephemeral`) 37 | - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) 38 | - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) 39 | - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) 40 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) 41 | - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) 42 | - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) 43 | - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) 44 | - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) 45 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 46 | - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) 47 | 48 | SPEC REPOS: 49 | trunk: 50 | - HotKey 51 | 52 | EXTERNAL SOURCES: 53 | bonsoir_darwin: 54 | :path: Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin 55 | device_info_plus: 56 | :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos 57 | FlutterMacOS: 58 | :path: Flutter/ephemeral 59 | hotkey_manager_macos: 60 | :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos 61 | irondash_engine_context: 62 | :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos 63 | package_info_plus: 64 | :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos 65 | path_provider_foundation: 66 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin 67 | screen_retriever_macos: 68 | :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos 69 | shared_preferences_foundation: 70 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin 71 | super_native_extensions: 72 | :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos 73 | tray_manager: 74 | :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos 75 | url_launcher_macos: 76 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 77 | window_manager: 78 | :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos 79 | 80 | SPEC CHECKSUMS: 81 | bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 82 | device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 83 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 84 | HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6 85 | hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 86 | irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 87 | package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b 88 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 89 | screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 90 | shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 91 | super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 92 | tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 93 | url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 94 | window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 95 | 96 | PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 97 | 98 | COCOAPODS: 1.16.2 99 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | { 32 | "associatedIndex": 6 33 | } 34 | 35 | 36 | 37 | 38 | 39 | 42 | { 43 | "keyToString": { 44 | "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", 45 | "ModuleVcsDetector.initialDetectionPerformed": "true", 46 | "RunOnceActivity.ShowReadmeOnStart": "true", 47 | "dart.analysis.tool.window.visible": "false", 48 | "git-widget-placeholder": "main", 49 | "last_opened_file_path": "/Users/apple/Desktop/Github/easy_pasta", 50 | "node.js.detected.package.eslint": "true", 51 | "node.js.detected.package.tslint": "true", 52 | "node.js.selected.package.eslint": "(autodetect)", 53 | "node.js.selected.package.tslint": "(autodetect)", 54 | "nodejs_package_manager_path": "npm", 55 | "settings.editor.selected.configurable": "editor.preferences.appearance", 56 | "vue.rearranger.settings.migration": "true" 57 | } 58 | } 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 1748063755488 74 | 83 | 84 | 85 | 86 | 88 | -------------------------------------------------------------------------------- /lib/page/home_page_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:easy_pasta/model/pasteboard_model.dart'; 4 | import 'package:easy_pasta/page/settings_page.dart'; 5 | import 'package:easy_pasta/page/grid_view.dart'; 6 | import 'package:easy_pasta/page/app_bar_view.dart'; 7 | import 'package:easy_pasta/providers/pboard_provider.dart'; 8 | import 'package:easy_pasta/core/super_clipboard.dart'; 9 | import 'package:easy_pasta/core/window_service.dart'; 10 | import 'package:easy_pasta/model/pboard_sort_type.dart'; 11 | import 'package:tray_manager/tray_manager.dart'; 12 | import 'package:window_manager/window_manager.dart'; 13 | 14 | class MyHomePage extends StatefulWidget { 15 | const MyHomePage({super.key}); 16 | @override 17 | State createState() => _MyHomePageState(); 18 | } 19 | 20 | class _MyHomePageState extends State 21 | with TrayListener, WindowListener { 22 | late final PboardProvider _pboardProvider; 23 | final _searchController = TextEditingController(); 24 | final _superClipboard = SuperClipboard.instance; 25 | 26 | NSPboardSortType _selectedType = NSPboardSortType.all; 27 | String _selectedId = ''; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | WidgetsBinding.instance.addPostFrameCallback((_) { 33 | context.read().loadItems(); 34 | }); 35 | trayManager.addListener(this); 36 | windowManager.addListener(this); 37 | _setupClipboardListener(); 38 | _pboardProvider = context.read(); 39 | } 40 | 41 | void _setupClipboardListener() { 42 | _superClipboard.setClipboardListener((value) { 43 | if (value != null) { 44 | _handlePboardUpdate(value); 45 | } 46 | }); 47 | } 48 | 49 | void _handlePboardUpdate(ClipboardItemModel model) { 50 | _pboardProvider.addItem(model); 51 | } 52 | 53 | void _handleClear() { 54 | _pboardProvider.loadItems(); 55 | } 56 | 57 | void _handleSearch(String value) { 58 | if (value.isEmpty) { 59 | _pboardProvider.loadItems(); 60 | } else { 61 | _pboardProvider.search(value); 62 | } 63 | } 64 | 65 | void _handleTypeChanged(NSPboardSortType type) { 66 | setState(() => _selectedType = type); 67 | _pboardProvider.filterByType(type); 68 | } 69 | 70 | void _handleItemTap(ClipboardItemModel model) { 71 | if (!mounted) return; 72 | setState(() => _selectedId = model.id); 73 | } 74 | 75 | void _handleFavorite(ClipboardItemModel model) { 76 | _pboardProvider.toggleFavorite(model); 77 | } 78 | 79 | void _handleDelete(ClipboardItemModel model) { 80 | _pboardProvider.delete(model); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return Scaffold( 86 | appBar: CustomAppBar( 87 | onSearch: _handleSearch, 88 | searchController: _searchController, 89 | onClear: _handleClear, 90 | onTypeChanged: _handleTypeChanged, 91 | selectedType: _selectedType, 92 | onSettingsTap: () => _navigateToSettings(), 93 | ), 94 | body: Consumer( 95 | builder: (context, provider, child) { 96 | if (provider.isLoading) { 97 | return const Center(child: CircularProgressIndicator()); 98 | } 99 | 100 | if (provider.error != null) { 101 | return Center( 102 | child: Text( 103 | provider.error!, 104 | style: const TextStyle(color: Colors.red), 105 | ), 106 | ); 107 | } 108 | 109 | return PasteboardGridView( 110 | pboards: provider.items, 111 | selectedId: _selectedId, 112 | onItemTap: _handleItemTap, 113 | onItemDoubleTap: _setPasteboardItem, 114 | onCopy: _setPasteboardItem, 115 | onFavorite: _handleFavorite, 116 | onDelete: _handleDelete, 117 | ); 118 | }, 119 | ), 120 | ); 121 | } 122 | 123 | void _navigateToSettings() { 124 | Navigator.push( 125 | context, 126 | MaterialPageRoute(builder: (_) => const SettingsPage()), 127 | ); 128 | } 129 | 130 | void _setPasteboardItem(ClipboardItemModel model) { 131 | _superClipboard.setPasteboardItem(model); 132 | WindowService().closeWindow(); 133 | } 134 | 135 | @override 136 | void dispose() { 137 | _searchController.dispose(); 138 | trayManager.removeListener(this); 139 | windowManager.removeListener(this); 140 | super.dispose(); 141 | } 142 | 143 | @override 144 | void onTrayIconMouseDown() { 145 | WindowService().showWindow(); 146 | } 147 | 148 | @override 149 | void onWindowBlur() => WindowService().closeWindow(); 150 | } 151 | -------------------------------------------------------------------------------- /lib/widget/setting_counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/db/shared_preference_helper.dart'; 3 | 4 | class ModernCounter extends StatefulWidget { 5 | final ValueChanged? onChanged; 6 | final int minValue; 7 | final int maxValue; 8 | final int defaultValue; 9 | 10 | const ModernCounter({ 11 | Key? key, 12 | this.onChanged, 13 | this.minValue = 10, 14 | this.maxValue = 500, 15 | this.defaultValue = 50, 16 | }) : super(key: key); 17 | 18 | @override 19 | State createState() => _ModernCounterState(); 20 | } 21 | 22 | class _ModernCounterState extends State { 23 | late int _count; 24 | bool _isLoading = true; 25 | bool _mounted = true; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _count = widget.defaultValue; 31 | _loadStoredValue(); 32 | } 33 | 34 | Future _loadStoredValue() async { 35 | if (!_mounted) return; 36 | try { 37 | final prefs = await SharedPreferenceHelper.instance; 38 | if (!_mounted) return; 39 | setState(() { 40 | _count = prefs.getMaxItemStore(); 41 | _isLoading = false; 42 | }); 43 | } catch (e) { 44 | if (!_mounted) return; 45 | setState(() { 46 | _count = widget.defaultValue; 47 | _isLoading = false; 48 | }); 49 | } 50 | } 51 | 52 | Future _updateValue(int newValue) async { 53 | if (!_mounted) return; 54 | if (newValue < widget.minValue || newValue > widget.maxValue) { 55 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 56 | content: Text(newValue < widget.minValue 57 | ? '最小不能低于 ${widget.minValue}' 58 | : '最大不能超过 ${widget.maxValue}'), 59 | behavior: SnackBarBehavior.floating, 60 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), 61 | )); 62 | return; 63 | } 64 | 65 | setState(() => _count = newValue); 66 | widget.onChanged?.call(newValue); 67 | 68 | try { 69 | final prefs = await SharedPreferenceHelper.instance; 70 | await prefs.setMaxItemStore(newValue); 71 | } catch (e) { 72 | if (!_mounted) return; 73 | // ignore: use_build_context_synchronously 74 | ScaffoldMessenger.of(context).showSnackBar( 75 | const SnackBar(content: Text('保存失败,请重试')), 76 | ); 77 | } 78 | } 79 | 80 | Widget _buildButton(bool isIncrement, ThemeData theme) { 81 | final isDark = theme.brightness == Brightness.dark; 82 | final isDisabled = 83 | isIncrement ? _count >= widget.maxValue : _count <= widget.minValue; 84 | 85 | return Container( 86 | width: 40, 87 | height: 40, 88 | decoration: BoxDecoration( 89 | color: isDisabled 90 | ? (isDark ? Colors.grey[800] : Colors.grey[200]) 91 | : theme.colorScheme.primary.withOpacity(isDark ? 0.2 : 0.1), 92 | borderRadius: BorderRadius.circular(12), 93 | ), 94 | child: IconButton( 95 | onPressed: isDisabled 96 | ? null 97 | : () => _updateValue(_count + (isIncrement ? 10 : -10)), 98 | icon: Icon( 99 | isIncrement ? Icons.add : Icons.remove, 100 | size: 20, 101 | color: isDisabled 102 | ? (isDark ? Colors.grey[600] : Colors.grey[400]) 103 | : theme.colorScheme.primary, 104 | ), 105 | splashRadius: 20, 106 | tooltip: isIncrement ? '增加' : '减少', 107 | ), 108 | ); 109 | } 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | if (_isLoading) { 114 | return const SizedBox( 115 | width: 150, 116 | child: Center(child: CircularProgressIndicator(strokeWidth: 2)), 117 | ); 118 | } 119 | 120 | final theme = Theme.of(context); 121 | 122 | return Container( 123 | decoration: BoxDecoration( 124 | color: theme.colorScheme.surface.withOpacity(0.1), 125 | borderRadius: BorderRadius.circular(16), 126 | ), 127 | child: Row( 128 | mainAxisSize: MainAxisSize.min, 129 | children: [ 130 | _buildButton(false, theme), 131 | Container( 132 | width: 40, 133 | margin: const EdgeInsets.symmetric(horizontal: 4), 134 | child: Text( 135 | _count.toString(), 136 | style: TextStyle( 137 | fontSize: 20, 138 | fontWeight: FontWeight.w600, 139 | color: theme.colorScheme.primary, 140 | ), 141 | textAlign: TextAlign.center, 142 | ), 143 | ), 144 | _buildButton(true, theme), 145 | ], 146 | ), 147 | ); 148 | } 149 | 150 | @override 151 | void dispose() { 152 | _mounted = false; 153 | super.dispose(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/page/grid_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:easy_pasta/model/pasteboard_model.dart'; 4 | import 'package:easy_pasta/page/pboard_card_view.dart'; 5 | import 'package:easy_pasta/page/empty_view.dart'; 6 | import 'package:easy_pasta/widget/preview_dialog.dart'; 7 | 8 | class PasteboardGridView extends StatefulWidget { 9 | static const double _kGridSpacing = 8.0; 10 | static const double _kMinCrossAxisExtent = 250.0; 11 | 12 | final List pboards; 13 | final String selectedId; 14 | final Function(ClipboardItemModel) onItemTap; 15 | final Function(ClipboardItemModel) onItemDoubleTap; 16 | final Function(ClipboardItemModel) onCopy; 17 | final Function(ClipboardItemModel) onFavorite; 18 | final Function(ClipboardItemModel) onDelete; 19 | 20 | const PasteboardGridView({ 21 | Key? key, 22 | required this.pboards, 23 | required this.selectedId, 24 | required this.onItemTap, 25 | required this.onItemDoubleTap, 26 | required this.onCopy, 27 | required this.onFavorite, 28 | required this.onDelete, 29 | }) : super(key: key); 30 | 31 | @override 32 | State createState() => _PasteboardGridViewState(); 33 | } 34 | 35 | class _PasteboardGridViewState extends State 36 | with AutomaticKeepAliveClientMixin { 37 | final ScrollController _scrollController = ScrollController(); 38 | ClipboardItemModel? _hoveredItem; 39 | final FocusNode _focusNode = FocusNode(); 40 | 41 | @override 42 | void dispose() { 43 | _scrollController.dispose(); 44 | _focusNode.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | void _showPreviewDialog(BuildContext context, ClipboardItemModel model) { 49 | print('showPreviewDialog'); 50 | PreviewDialog.show(context, model); 51 | } 52 | 53 | /// 计算网格列数和宽度 54 | int _calculateMaxColumns(double maxWidth) { 55 | return (maxWidth / PasteboardGridView._kMinCrossAxisExtent) 56 | .floor() 57 | .clamp(1, 3); 58 | } 59 | 60 | /// 计算网格的纵横比 61 | double _calculateAspectRatio(double itemWidth) { 62 | return itemWidth / (itemWidth / 1.2); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | super.build(context); 68 | 69 | if (widget.pboards.isEmpty) { 70 | return const EmptyStateView(); 71 | } 72 | 73 | return KeyboardListener( 74 | focusNode: _focusNode, 75 | onKeyEvent: (event) { 76 | if (event.logicalKey == LogicalKeyboardKey.space && 77 | _hoveredItem != null) { 78 | _showPreviewDialog(context, _hoveredItem!); 79 | } 80 | }, 81 | child: LayoutBuilder( 82 | builder: (context, constraints) { 83 | final maxColumns = _calculateMaxColumns(constraints.maxWidth); 84 | final itemWidth = (constraints.maxWidth - 85 | (maxColumns - 1) * PasteboardGridView._kGridSpacing) / 86 | maxColumns; 87 | final aspectRatio = _calculateAspectRatio(itemWidth); 88 | 89 | return Scrollbar( 90 | controller: _scrollController, 91 | child: GridView.builder( 92 | key: const PageStorageKey('pasteboard_grid'), 93 | controller: _scrollController, 94 | physics: const BouncingScrollPhysics( 95 | parent: AlwaysScrollableScrollPhysics(), 96 | ), 97 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 98 | crossAxisCount: maxColumns, 99 | mainAxisSpacing: PasteboardGridView._kGridSpacing, 100 | crossAxisSpacing: PasteboardGridView._kGridSpacing, 101 | childAspectRatio: aspectRatio, 102 | ), 103 | cacheExtent: 1000, 104 | itemCount: widget.pboards.length, 105 | itemBuilder: (context, index) { 106 | final model = widget.pboards[index]; 107 | return MouseRegion( 108 | onEnter: (_) => _updateHoveredItem(model, true), 109 | onExit: (_) => _updateHoveredItem(null, false), 110 | child: NewPboardItemCard( 111 | key: ValueKey(model.id), 112 | model: model, 113 | selectedId: widget.selectedId, 114 | onTap: widget.onItemTap, 115 | onDoubleTap: widget.onItemDoubleTap, 116 | onCopy: widget.onCopy, 117 | onFavorite: widget.onFavorite, 118 | onDelete: widget.onDelete, 119 | ), 120 | ); 121 | }, 122 | ), 123 | ); 124 | }, 125 | ), 126 | ); 127 | } 128 | 129 | /// 更新鼠标悬停的项目 130 | void _updateHoveredItem(ClipboardItemModel? model, bool isHovered) { 131 | setState(() { 132 | _hoveredItem = isHovered ? model : null; 133 | if (isHovered) { 134 | _focusNode.requestFocus(); 135 | } else { 136 | _focusNode.unfocus(); 137 | } 138 | }); 139 | } 140 | 141 | @override 142 | bool get wantKeepAlive => true; 143 | } 144 | -------------------------------------------------------------------------------- /lib/widget/cards/file_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A widget that displays a file icon and name in a vertical layout 4 | class FileContent extends StatelessWidget { 5 | final String fileName; 6 | final String fileUri; 7 | 8 | // Constants for styling 9 | static const double _iconSize = 56.0; 10 | static const double _fontSize = 13.0; 11 | 12 | const FileContent({ 13 | super.key, 14 | required this.fileName, 15 | required this.fileUri, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Column( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | children: [ 23 | _buildIcon(), 24 | const SizedBox(height: 8), 25 | _buildFileName(), 26 | ], 27 | ); 28 | } 29 | 30 | Widget _buildIcon() { 31 | return Icon( 32 | _getFileIcon(), 33 | size: _iconSize, 34 | color: _getIconColor(), 35 | ); 36 | } 37 | 38 | Widget _buildFileName() { 39 | return Text( 40 | fileName, 41 | textAlign: TextAlign.center, 42 | maxLines: 2, 43 | overflow: TextOverflow.ellipsis, 44 | style: const TextStyle( 45 | fontSize: _fontSize, 46 | height: 1.2, 47 | ), 48 | ); 49 | } 50 | 51 | IconData _getFileIcon() { 52 | if (_isDirectory()) { 53 | return Icons.folder; 54 | } 55 | final extension = _getFileExtension(); 56 | return _fileIconMap[extension] ?? Icons.insert_drive_file; 57 | } 58 | 59 | Color _getIconColor() { 60 | if (_isDirectory()) { 61 | return Colors.blue[600]!; 62 | } 63 | final extension = _getFileExtension(); 64 | return _fileColorMap[extension] ?? Colors.grey[600]!; 65 | } 66 | 67 | bool _isDirectory() { 68 | final cleanPath = fileUri.replaceAll(RegExp(r'[/\\]+$'), ''); 69 | final hasExtension = cleanPath.split('/').last.contains('.'); 70 | return !hasExtension; 71 | } 72 | 73 | String _getFileExtension() => fileUri.split('.').length > 1 74 | ? fileUri.split('.').last.toLowerCase() 75 | : ''; 76 | 77 | // File type to icon mapping 78 | static final Map _fileIconMap = { 79 | // Documents 80 | 'pdf': Icons.picture_as_pdf, 81 | 'doc': Icons.description, 82 | 'docx': Icons.description, 83 | 'txt': Icons.article, 84 | 'rtf': Icons.article, 85 | 86 | // Spreadsheets 87 | 'xls': Icons.table_chart, 88 | 'xlsx': Icons.table_chart, 89 | 'csv': Icons.table_chart, 90 | 91 | // Images 92 | 'jpg': Icons.image, 93 | 'jpeg': Icons.image, 94 | 'png': Icons.image, 95 | 'gif': Icons.gif, 96 | 'svg': Icons.image, 97 | 'webp': Icons.image, 98 | 'bmp': Icons.image, 99 | 100 | // Audio 101 | 'mp3': Icons.audio_file, 102 | 'wav': Icons.audio_file, 103 | 'aac': Icons.audio_file, 104 | 'm4a': Icons.audio_file, 105 | 'ogg': Icons.audio_file, 106 | 107 | // Video 108 | 'mp4': Icons.video_file, 109 | 'mov': Icons.video_file, 110 | 'avi': Icons.video_file, 111 | 'mkv': Icons.video_file, 112 | 'wmv': Icons.video_file, 113 | 114 | // Archives 115 | 'zip': Icons.folder_zip, 116 | 'rar': Icons.folder_zip, 117 | '7z': Icons.folder_zip, 118 | 'tar': Icons.folder_zip, 119 | 'gz': Icons.folder_zip, 120 | 121 | // Code 122 | 'html': Icons.code, 123 | 'css': Icons.code, 124 | 'js': Icons.code, 125 | 'json': Icons.code, 126 | 'xml': Icons.code, 127 | 'py': Icons.code, 128 | 'java': Icons.code, 129 | 'cpp': Icons.code, 130 | 131 | // Others 132 | 'exe': Icons.run_circle_outlined, 133 | 'apk': Icons.android, 134 | 'iso': Icons.disc_full, 135 | 'torrent': Icons.download, 136 | }; 137 | 138 | // File type to color mapping 139 | static final Map _fileColorMap = { 140 | // Documents - Blue shades 141 | 'pdf': Colors.red[400]!, 142 | 'doc': Colors.blue[600]!, 143 | 'docx': Colors.blue[600]!, 144 | 'txt': Colors.blue[400]!, 145 | 'rtf': Colors.blue[400]!, 146 | 147 | // Spreadsheets - Green shades 148 | 'xls': Colors.green[600]!, 149 | 'xlsx': Colors.green[600]!, 150 | 'csv': Colors.green[400]!, 151 | 152 | // Images - Purple shades 153 | 'jpg': Colors.purple[400]!, 154 | 'jpeg': Colors.purple[400]!, 155 | 'png': Colors.purple[400]!, 156 | 'gif': Colors.purple[500]!, 157 | 'svg': Colors.purple[600]!, 158 | 159 | // Audio - Orange shades 160 | 'mp3': Colors.orange[400]!, 161 | 'wav': Colors.orange[400]!, 162 | 'aac': Colors.orange[400]!, 163 | 'm4a': Colors.orange[400]!, 164 | 165 | // Video - Red shades 166 | 'mp4': Colors.red[400]!, 167 | 'mov': Colors.red[400]!, 168 | 'avi': Colors.red[400]!, 169 | 'mkv': Colors.red[400]!, 170 | 171 | // Archives - Brown shades 172 | 'zip': Colors.brown[400]!, 173 | 'rar': Colors.brown[400]!, 174 | '7z': Colors.brown[400]!, 175 | 176 | // Code - Grey shades 177 | 'html': Colors.blueGrey[400]!, 178 | 'css': Colors.blueGrey[400]!, 179 | 'js': Colors.blueGrey[400]!, 180 | 'py': Colors.blueGrey[400]!, 181 | 'java': Colors.blueGrey[400]!, 182 | }; 183 | } 184 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.10) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "easy_pasta") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.example.easy_pasta") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 58 | 59 | # Define the application target. To change its name, change BINARY_NAME above, 60 | # not the value here, or `flutter run` will no longer work. 61 | # 62 | # Any new source files that you add to the application should be added here. 63 | add_executable(${BINARY_NAME} 64 | "main.cc" 65 | "my_application.cc" 66 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 67 | ) 68 | 69 | # Apply the standard set of build settings. This can be removed for applications 70 | # that need different build settings. 71 | apply_standard_settings(${BINARY_NAME}) 72 | 73 | # Add dependency libraries. Add any application-specific dependencies here. 74 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 75 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 76 | 77 | # Run the Flutter tool portions of the build. This must not be removed. 78 | add_dependencies(${BINARY_NAME} flutter_assemble) 79 | 80 | # Only the install-generated bundle's copy of the executable will launch 81 | # correctly, since the resources must in the right relative locations. To avoid 82 | # people trying to run the unbundled copy, put it in a subdirectory instead of 83 | # the default top-level location. 84 | set_target_properties(${BINARY_NAME} 85 | PROPERTIES 86 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 87 | ) 88 | 89 | 90 | # Generated plugin build rules, which manage building the plugins and adding 91 | # them to the application. 92 | include(flutter/generated_plugins.cmake) 93 | 94 | 95 | # === Installation === 96 | # By default, "installing" just makes a relocatable bundle in the build 97 | # directory. 98 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 99 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 100 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 101 | endif() 102 | 103 | # Start with a clean build bundle directory every time. 104 | install(CODE " 105 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 106 | " COMPONENT Runtime) 107 | 108 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 109 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 110 | 111 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 112 | COMPONENT Runtime) 113 | 114 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 115 | COMPONENT Runtime) 116 | 117 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 118 | COMPONENT Runtime) 119 | 120 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 121 | install(FILES "${bundled_library}" 122 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 123 | COMPONENT Runtime) 124 | endforeach(bundled_library) 125 | 126 | # Fully re-copy the assets directory on each build to avoid having stale files 127 | # from a previous install. 128 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 129 | install(CODE " 130 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 131 | " COMPONENT Runtime) 132 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 133 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 134 | 135 | # Install the AOT library on non-Debug builds only. 136 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 137 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 138 | COMPONENT Runtime) 139 | endif() 140 | -------------------------------------------------------------------------------- /lib/page/app_bar_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:easy_pasta/model/pboard_sort_type.dart'; 3 | 4 | class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { 5 | final ValueChanged onSearch; 6 | final TextEditingController searchController; 7 | final VoidCallback onClear; 8 | final ValueChanged onTypeChanged; 9 | final NSPboardSortType selectedType; 10 | final VoidCallback onSettingsTap; 11 | 12 | const CustomAppBar({ 13 | super.key, 14 | required this.onSearch, 15 | required this.searchController, 16 | required this.onClear, 17 | required this.onTypeChanged, 18 | required this.selectedType, 19 | required this.onSettingsTap, 20 | }); 21 | 22 | @override 23 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Padding( 28 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), 29 | child: Row( 30 | children: [ 31 | Expanded( 32 | flex: 2, 33 | child: _SearchField( 34 | controller: searchController, 35 | onSearch: onSearch, 36 | onClear: onClear, 37 | ), 38 | ), 39 | const SizedBox(width: 8), 40 | Expanded( 41 | flex: 3, 42 | child: _FilterBar( 43 | selectedType: selectedType, 44 | onTypeChanged: onTypeChanged, 45 | ), 46 | ), 47 | const SizedBox(width: 8), 48 | _SettingsButton(onTap: onSettingsTap), 49 | ], 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | class _SearchField extends StatelessWidget { 56 | final TextEditingController controller; 57 | final ValueChanged onSearch; 58 | final VoidCallback onClear; 59 | 60 | const _SearchField({ 61 | required this.controller, 62 | required this.onSearch, 63 | required this.onClear, 64 | }); 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | final theme = Theme.of(context); 69 | final isDark = theme.brightness == Brightness.dark; 70 | 71 | return SearchBar( 72 | controller: controller, 73 | onSubmitted: onSearch, 74 | hintText: '搜索', 75 | hintStyle: WidgetStateProperty.all( 76 | TextStyle(color: isDark ? Colors.white60 : Colors.black45), 77 | ), 78 | leading: Icon( 79 | Icons.search, 80 | size: 20, 81 | color: isDark ? Colors.white54 : Colors.black45, 82 | ), 83 | trailing: [ 84 | ValueListenableBuilder( 85 | valueListenable: controller, 86 | builder: (context, value, _) { 87 | if (value.text.isEmpty) return const SizedBox.shrink(); 88 | return IconButton( 89 | icon: const Icon(Icons.clear, size: 20), 90 | onPressed: () { 91 | controller.clear(); 92 | onClear(); 93 | }, 94 | ); 95 | }, 96 | ), 97 | ], 98 | elevation: WidgetStateProperty.all(0), 99 | backgroundColor: WidgetStateProperty.all( 100 | isDark ? Colors.grey[800] : Colors.grey[50], 101 | ), 102 | shape: WidgetStateProperty.all( 103 | RoundedRectangleBorder( 104 | borderRadius: BorderRadius.circular(8), 105 | side: BorderSide( 106 | color: isDark ? Colors.grey[600]! : Colors.grey[300]!, 107 | width: 1, 108 | ), 109 | ), 110 | ), 111 | padding: WidgetStateProperty.all( 112 | const EdgeInsets.symmetric(horizontal: 8), 113 | ), 114 | ); 115 | } 116 | } 117 | 118 | class _FilterBar extends StatelessWidget { 119 | final NSPboardSortType selectedType; 120 | final ValueChanged onTypeChanged; 121 | 122 | const _FilterBar({ 123 | required this.selectedType, 124 | required this.onTypeChanged, 125 | }); 126 | 127 | @override 128 | Widget build(BuildContext context) { 129 | return SingleChildScrollView( 130 | scrollDirection: Axis.horizontal, 131 | child: Row( 132 | mainAxisSize: MainAxisSize.min, 133 | children: [ 134 | for (final option in filterOptions) 135 | Padding( 136 | padding: const EdgeInsets.only(right: 8), 137 | child: _FilterChip( 138 | option: option, 139 | isSelected: selectedType == option.type, 140 | onSelected: onTypeChanged, 141 | ), 142 | ), 143 | ], 144 | ), 145 | ); 146 | } 147 | } 148 | 149 | class _FilterChip extends StatelessWidget { 150 | final FilterOption option; 151 | final bool isSelected; 152 | final ValueChanged onSelected; 153 | 154 | const _FilterChip({ 155 | required this.option, 156 | required this.isSelected, 157 | required this.onSelected, 158 | }); 159 | 160 | @override 161 | Widget build(BuildContext context) { 162 | final theme = Theme.of(context); 163 | 164 | return FilterChip( 165 | label: Row( 166 | mainAxisSize: MainAxisSize.min, 167 | children: [ 168 | Icon( 169 | option.icon, 170 | size: 16, 171 | color: isSelected ? Colors.white : theme.iconTheme.color, 172 | ), 173 | const SizedBox(width: 4), 174 | Text( 175 | option.label, 176 | style: TextStyle( 177 | color: 178 | isSelected ? Colors.white : theme.textTheme.bodyMedium?.color, 179 | fontSize: 13, 180 | ), 181 | ), 182 | ], 183 | ), 184 | selected: isSelected, 185 | onSelected: (_) => onSelected(option.type), 186 | selectedColor: theme.colorScheme.primary, 187 | showCheckmark: false, 188 | padding: const EdgeInsets.symmetric(horizontal: 8), 189 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 190 | ); 191 | } 192 | } 193 | 194 | class _SettingsButton extends StatelessWidget { 195 | final VoidCallback onTap; 196 | 197 | const _SettingsButton({required this.onTap}); 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | return IconButton( 202 | icon: const Icon(Icons.settings), 203 | onPressed: onTap, 204 | tooltip: '设置', 205 | constraints: const BoxConstraints(minWidth: 40), 206 | padding: EdgeInsets.zero, 207 | ); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/core/super_clipboard.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:super_clipboard/super_clipboard.dart'; 6 | import 'package:easy_pasta/model/pasteboard_model.dart'; 7 | import 'package:easy_pasta/model/clipboard_type.dart'; 8 | import 'package:easy_pasta/core/html_processor.dart'; 9 | 10 | /// A singleton class that manages system clipboard operations and monitoring 11 | class SuperClipboard { 12 | // Singleton implementation 13 | static final SuperClipboard _instance = SuperClipboard._internal(); 14 | static SuperClipboard get instance => _instance; 15 | SuperClipboard._internal() { 16 | _startPollingTimer(); 17 | } 18 | 19 | final SystemClipboard? _clipboard = SystemClipboard.instance; 20 | ValueChanged? _onClipboardChanged; 21 | ClipboardItemModel? _lastContent; 22 | Timer? _pollingTimer; 23 | 24 | static const Duration _pollingInterval = Duration(seconds: 1); 25 | 26 | /// Starts monitoring clipboard changes 27 | void _startPollingTimer() { 28 | _pollingTimer?.cancel(); 29 | _pollingTimer = Timer.periodic(_pollingInterval, (_) => _pollClipboard()); 30 | } 31 | 32 | /// Polls clipboard content for changes 33 | Future _pollClipboard() async { 34 | try { 35 | final reader = await _clipboard?.read(); 36 | if (reader == null) return; 37 | 38 | await _processClipboardContent(reader); 39 | } catch (e) { 40 | debugPrint('Clipboard polling error: $e'); 41 | } 42 | } 43 | 44 | /// Processes different types of clipboard content 45 | Future _processClipboardContent(ClipboardReader reader) async { 46 | if (await _processHtmlContent(reader)) return; 47 | if (await _processFileContent(reader)) return; 48 | if (await _processTextContent(reader)) return; 49 | if (await _processImageContent(reader)) return; 50 | } 51 | 52 | /// Processes HTML content from clipboard 53 | Future _processHtmlContent(ClipboardReader reader) async { 54 | if (!reader.canProvide(Formats.htmlText)) return false; 55 | 56 | final html = await reader.readValue(Formats.htmlText); 57 | final htmlPlainText = await reader.readValue(Formats.plainText); 58 | 59 | if (html != null) { 60 | final processedHtml = HtmlProcessor.processHtml(html.toString()); 61 | _handleContentChange(htmlPlainText.toString(), ClipboardType.html, 62 | bytes: Uint8List.fromList(utf8.encode(processedHtml))); 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | /// Processes file URI content from clipboard 69 | Future _processFileContent(ClipboardReader reader) async { 70 | if (!reader.canProvide(Formats.fileUri)) return false; 71 | 72 | final fileUri = await reader.readValue(Formats.fileUri); 73 | final fileUriString = await reader.readValue(Formats.plainText); 74 | 75 | if (fileUri != null) { 76 | _handleContentChange(fileUriString.toString(), ClipboardType.file, 77 | bytes: Uint8List.fromList(utf8.encode(fileUri.toString()))); 78 | return true; 79 | } 80 | return false; 81 | } 82 | 83 | /// Processes plain text content from clipboard 84 | Future _processTextContent(ClipboardReader reader) async { 85 | if (!reader.canProvide(Formats.plainText)) return false; 86 | 87 | final text = await reader.readValue(Formats.plainText); 88 | if (text != null) { 89 | _handleContentChange(text.toString(), ClipboardType.text); 90 | return true; 91 | } 92 | return false; 93 | } 94 | 95 | /// Processes image content from clipboard 96 | Future _processImageContent(ClipboardReader reader) async { 97 | if (!reader.canProvide(Formats.png)) return false; 98 | 99 | try { 100 | final completer = Completer(); 101 | 102 | reader.getFile(Formats.png, (file) async { 103 | try { 104 | final stream = file.getStream(); 105 | final bytes = await stream.toList(); 106 | final imageData = bytes.expand((x) => x).toList(); 107 | _handleContentChange('', ClipboardType.image, 108 | bytes: Uint8List.fromList(imageData)); 109 | completer.complete(true); 110 | } catch (e) { 111 | debugPrint('Error processing image: $e'); 112 | completer.complete(false); 113 | } 114 | }); 115 | 116 | return await completer.future; 117 | } catch (e) { 118 | debugPrint('Error accessing image file: $e'); 119 | return false; 120 | } 121 | } 122 | 123 | /// Handles content changes and notifies listeners 124 | void _handleContentChange(String content, ClipboardType? type, 125 | {Uint8List? bytes}) { 126 | final contentModel = _createContentModel(content, type, bytes); 127 | 128 | if (contentModel != _lastContent) { 129 | _lastContent = contentModel; 130 | _onClipboardChanged?.call(contentModel); 131 | } 132 | } 133 | 134 | /// Creates a content model based on the clipboard type 135 | ClipboardItemModel _createContentModel( 136 | String content, ClipboardType? type, Uint8List? bytes) { 137 | return ClipboardItemModel( 138 | ptype: type, 139 | pvalue: content, 140 | bytes: type == ClipboardType.text ? null : bytes, 141 | ); 142 | } 143 | 144 | /// Sets clipboard change listener 145 | void setClipboardListener(ValueChanged listener) { 146 | _onClipboardChanged = listener; 147 | } 148 | 149 | /// Writes content to clipboard 150 | Future setPasteboardItem(ClipboardItemModel model) => setContent(model); 151 | 152 | /// Writes content to clipboard with proper format 153 | Future setContent(ClipboardItemModel model) async { 154 | final item = DataWriterItem(); 155 | 156 | switch (model.ptype) { 157 | case ClipboardType.html: 158 | item.add(Formats.plainText(model.pvalue)); 159 | item.add( 160 | Formats.htmlText(model.bytesToString(model.bytes ?? Uint8List(0)))); 161 | break; 162 | case ClipboardType.file: 163 | item.add(Formats.plainText(model.pvalue)); 164 | item.add(Formats.fileUri( 165 | Uri.parse(model.bytesToString(model.bytes ?? Uint8List(0))))); 166 | break; 167 | case ClipboardType.text: 168 | item.add(Formats.plainText(model.pvalue)); 169 | break; 170 | case ClipboardType.image: 171 | item.add( 172 | Formats.png(Uint8List.fromList(model.imageBytes ?? Uint8List(0)))); 173 | break; 174 | default: 175 | throw ArgumentError('Unsupported clipboard type: ${model.ptype}'); 176 | } 177 | 178 | try { 179 | await _clipboard?.write([item]); 180 | } catch (e) { 181 | debugPrint('Failed to write to clipboard: $e'); 182 | rethrow; 183 | } 184 | } 185 | 186 | /// Cleans up resources 187 | void dispose() { 188 | _pollingTimer?.cancel(); 189 | _pollingTimer = null; 190 | _onClipboardChanged = null; 191 | _lastContent = null; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/page/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hotkey_manager/hotkey_manager.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:easy_pasta/model/settings_constants.dart'; 5 | import 'package:easy_pasta/model/settings_model.dart'; 6 | import 'package:easy_pasta/providers/theme_provider.dart'; 7 | import 'package:easy_pasta/widget/setting_tiles.dart' hide ThemeTile, HotkeyTile; 8 | import 'package:easy_pasta/core/settings_service.dart'; 9 | import 'package:easy_pasta/page/confirm_dialog_view.dart'; 10 | import 'package:easy_pasta/widget/settting_page_widgets.dart'; 11 | 12 | class SettingsPage extends StatefulWidget { 13 | const SettingsPage({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _SettingsPageState(); 17 | } 18 | 19 | class _SettingsPageState extends State { 20 | final _settingsService = SettingsService(); 21 | bool _autoLaunch = false; 22 | bool _bonjourEnabled = false; 23 | HotKey? _hotKey; 24 | 25 | final List _basicSettings = [ 26 | const SettingItem( 27 | type: SettingType.hotkey, 28 | title: SettingsConstants.hotkeyTitle, 29 | subtitle: SettingsConstants.hotkeySubtitle, 30 | icon: Icons.keyboard, 31 | ), 32 | const SettingItem( 33 | type: SettingType.theme, 34 | title: SettingsConstants.themeTitle, 35 | subtitle: SettingsConstants.themeSubtitle, 36 | icon: Icons.palette, 37 | ), 38 | const SettingItem( 39 | type: SettingType.autoLaunch, 40 | title: SettingsConstants.autoLaunchTitle, 41 | subtitle: SettingsConstants.autoLaunchSubtitle, 42 | icon: Icons.launch, 43 | ), 44 | const SettingItem( 45 | type: SettingType.maxStorage, 46 | title: SettingsConstants.maxStorageTitle, 47 | subtitle: SettingsConstants.maxStorageSubtitle, 48 | icon: Icons.storage, 49 | ), 50 | const SettingItem( 51 | type: SettingType.bonjour, 52 | title: SettingsConstants.bonjourTitle, 53 | subtitle: SettingsConstants.bonjourSubtitle, 54 | icon: Icons.network_wifi, 55 | ), 56 | const SettingItem( 57 | type: SettingType.clearData, 58 | title: SettingsConstants.clearDataTitle, 59 | subtitle: SettingsConstants.clearDataSubtitle, 60 | icon: Icons.delete_outline, 61 | iconColor: Colors.red, 62 | textColor: Colors.red, 63 | ), 64 | const SettingItem( 65 | type: SettingType.exitApp, 66 | title: SettingsConstants.exitAppTitle, 67 | subtitle: SettingsConstants.exitAppSubtitle, 68 | icon: Icons.exit_to_app, 69 | iconColor: Colors.red, 70 | textColor: Colors.red, 71 | ), 72 | ]; 73 | 74 | @override 75 | void initState() { 76 | super.initState(); 77 | _loadSettings(); 78 | } 79 | 80 | Future _loadSettings() async { 81 | _hotKey = await _settingsService.getHotKey(); 82 | _autoLaunch = await _settingsService.getAutoLaunch(); 83 | setState(() {}); 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return Scaffold( 89 | appBar: AppBar( 90 | title: const Text('设置'), 91 | centerTitle: true, 92 | elevation: 0, 93 | ), 94 | body: ListView( 95 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 96 | children: [ 97 | _buildSection( 98 | title: SettingsConstants.basicSettingsTitle, 99 | items: _basicSettings, 100 | ), 101 | const SizedBox(height: 32), 102 | _buildSection( 103 | title: SettingsConstants.aboutTitle, 104 | items: [ 105 | const SettingItem( 106 | type: SettingType.about, 107 | title: SettingsConstants.versionInfoTitle, 108 | subtitle: SettingsConstants.versionInfoSubtitle, 109 | icon: Icons.info_outline, 110 | ), 111 | ], 112 | ), 113 | ], 114 | ), 115 | ); 116 | } 117 | 118 | Widget _buildSection({ 119 | required String title, 120 | required List items, 121 | }) { 122 | return Column( 123 | crossAxisAlignment: CrossAxisAlignment.start, 124 | children: [ 125 | Padding( 126 | padding: const EdgeInsets.all(16), 127 | child: Text( 128 | title, 129 | ), 130 | ), 131 | Card( 132 | margin: const EdgeInsets.symmetric(horizontal: 8), 133 | child: Column( 134 | children: items.map((item) => _buildSettingTile(item)).toList(), 135 | ), 136 | ), 137 | ], 138 | ); 139 | } 140 | 141 | Widget _buildSettingTile(SettingItem item) { 142 | switch (item.type) { 143 | case SettingType.hotkey: 144 | return HotkeyTile( 145 | item: item, 146 | hotKey: _hotKey, 147 | onHotKeyChanged: (newHotKey) async { 148 | await _settingsService.setHotKey(newHotKey); 149 | setState(() => _hotKey = newHotKey); 150 | }, 151 | ); 152 | case SettingType.theme: 153 | return Consumer( 154 | builder: (context, themeProvider, _) { 155 | return ThemeTile( 156 | item: item, 157 | currentThemeMode: themeProvider.themeMode, 158 | onThemeModeChanged: (ThemeMode mode) { 159 | themeProvider.setThemeMode(mode); 160 | }, 161 | ); 162 | }, 163 | ); 164 | case SettingType.autoLaunch: 165 | return AutoLaunchTile( 166 | item: item, 167 | value: _autoLaunch, 168 | onChanged: (value) async { 169 | await _settingsService.setAutoLaunch(value); 170 | setState(() => _autoLaunch = value); 171 | }, 172 | ); 173 | case SettingType.bonjour: // Handle the new Bonjour setting type 174 | return SwitchListTile( 175 | title: Text(item.title), 176 | subtitle: Text(item.subtitle), 177 | value: _bonjourEnabled, 178 | onChanged: (bool value) async { 179 | setState(() { 180 | _bonjourEnabled = value; 181 | }); 182 | // Here you could add logic to start or stop Bonjour service 183 | // For example: _startBonjourService(value); 184 | }, 185 | secondary: Icon(item.icon), 186 | ); 187 | case SettingType.maxStorage: 188 | return MaxStorageTile(item: item); 189 | case SettingType.clearData: 190 | return ClearDataTile( 191 | item: item, 192 | onClear: () => _showClearConfirmDialog(), 193 | ); 194 | case SettingType.exitApp: 195 | return ExitAppTile(item: item); 196 | case SettingType.about: 197 | return AboutTile(item: item); 198 | } 199 | } 200 | 201 | Future _showClearConfirmDialog() async { 202 | final result = await showDialog( 203 | context: context, 204 | builder: (context) => const ConfirmDialog( 205 | title: '确认清除', 206 | content: '是否清除所有剪贴板记录?此操作不可恢复。', 207 | confirmText: '确定', 208 | cancelText: '取消', 209 | ), 210 | ); 211 | 212 | if (result == true && mounted) { 213 | await _settingsService.clearAllData(context); 214 | if (mounted) { 215 | Navigator.pop(context); 216 | } 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /lib/core/bonsoir_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:bonsoir/bonsoir.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | /// Bonjour/mDNS 服务管理单例 5 | /// 用于 EasyPasta 跨设备剪贴板同步功能 6 | class BonjourManager { 7 | static BonjourManager? _instance; 8 | static final _lock = Object(); 9 | 10 | // 服务配置 11 | static const String _serviceType = '_easypasta._tcp'; 12 | static const String _serviceName = 'EasyPasta-Clipboard'; 13 | static const int _defaultPort = 8888; 14 | 15 | BonsoirService? _service; 16 | BonsoirBroadcast? _broadcast; 17 | BonsoirDiscovery? _discovery; 18 | 19 | // 发现的设备列表 - 使用 service name 作为 key 20 | final Map _discoveredServices = {}; 21 | final Map _resolvedServices = {}; 22 | 23 | // 状态回调 24 | Function(List)? onServicesFound; 25 | Function(List)? onServicesResolved; 26 | Function(BonsoirService)? onServiceLost; 27 | Function(String message)? onError; 28 | Function(bool isRunning)? onServiceStateChanged; 29 | Function(bool isDiscovering)? onDiscoveryStateChanged; 30 | 31 | // 私有构造函数 32 | BonjourManager._internal(); 33 | 34 | /// 获取单例实例 35 | static BonjourManager get instance { 36 | if (_instance == null) { 37 | synchronized(_lock, () { 38 | _instance ??= BonjourManager._internal(); 39 | }); 40 | } 41 | return _instance!; 42 | } 43 | 44 | /// 启动 Bonjour 服务(广播本设备) 45 | Future startService({ 46 | String? deviceName, 47 | int port = _defaultPort, 48 | Map? attributes, 49 | }) async { 50 | try { 51 | await stopService(); // 先停止现有服务 52 | 53 | final Map serviceAttributes = { 54 | 'device_name': deviceName ?? 'EasyPasta Device', 55 | 'version': '1.0.0', 56 | 'timestamp': DateTime.now().millisecondsSinceEpoch.toString(), 57 | ...?attributes, 58 | }; 59 | 60 | _service = BonsoirService( 61 | name: deviceName ?? _serviceName, 62 | type: _serviceType, 63 | port: port, 64 | attributes: serviceAttributes, 65 | ); 66 | 67 | _broadcast = BonsoirBroadcast(service: _service!); 68 | 69 | await _broadcast!.ready; 70 | await _broadcast!.start(); 71 | 72 | if (kDebugMode) { 73 | print('EasyPasta Bonjour 服务已启动: ${_service!.name}'); 74 | } 75 | 76 | onServiceStateChanged?.call(true); 77 | return true; 78 | 79 | } catch (e) { 80 | final error = 'Bonjour 服务启动失败: $e'; 81 | if (kDebugMode) print(error); 82 | onError?.call(error); 83 | return false; 84 | } 85 | } 86 | 87 | /// 停止 Bonjour 服务 88 | Future stopService() async { 89 | try { 90 | if (_broadcast != null) { 91 | await _broadcast!.stop(); 92 | _broadcast = null; 93 | } 94 | _service = null; 95 | 96 | onServiceStateChanged?.call(false); 97 | 98 | if (kDebugMode) { 99 | print('EasyPasta Bonjour 服务已停止'); 100 | } 101 | } catch (e) { 102 | if (kDebugMode) print('停止 Bonjour 服务时出错: $e'); 103 | } 104 | } 105 | 106 | /// 开始发现其他设备 107 | Future startDiscovery() async { 108 | try { 109 | await stopDiscovery(); // 先停止现有发现 110 | 111 | _discovery = BonsoirDiscovery(type: _serviceType); 112 | 113 | await _discovery!.ready; 114 | 115 | // 监听发现的服务 - 必须在 start() 之前设置监听 116 | _discovery!.eventStream!.listen((event) { 117 | if (event.type == BonsoirDiscoveryEventType.discoveryServiceFound) { 118 | _handleServiceFound(event.service!); 119 | } else if (event.type == BonsoirDiscoveryEventType.discoveryServiceResolved) { 120 | _handleServiceResolved(event.service!); 121 | } else if (event.type == BonsoirDiscoveryEventType.discoveryServiceLost) { 122 | _handleServiceLost(event.service!); 123 | } 124 | }); 125 | 126 | await _discovery!.start(); 127 | 128 | onDiscoveryStateChanged?.call(true); 129 | 130 | if (kDebugMode) { 131 | print('开始发现 EasyPasta 设备...'); 132 | } 133 | 134 | return true; 135 | 136 | } catch (e) { 137 | final error = '设备发现启动失败: $e'; 138 | if (kDebugMode) print(error); 139 | onError?.call(error); 140 | return false; 141 | } 142 | } 143 | 144 | /// 停止设备发现 145 | Future stopDiscovery() async { 146 | try { 147 | if (_discovery != null) { 148 | await _discovery!.stop(); 149 | _discovery = null; 150 | } 151 | 152 | _discoveredServices.clear(); 153 | _resolvedServices.clear(); 154 | 155 | onDiscoveryStateChanged?.call(false); 156 | 157 | if (kDebugMode) { 158 | print('设备发现已停止'); 159 | } 160 | } catch (e) { 161 | if (kDebugMode) print('停止设备发现时出错: $e'); 162 | } 163 | } 164 | 165 | /// 处理发现的服务 166 | void _handleServiceFound(BonsoirService service) { 167 | _discoveredServices[service.name] = service; 168 | 169 | if (kDebugMode) { 170 | print('发现 EasyPasta 设备: ${service.name}'); 171 | print('服务信息: ${service.toJson()}'); 172 | } 173 | 174 | onServicesFound?.call(_discoveredServices.values.toList()); 175 | } 176 | 177 | /// 处理已解析的服务 178 | void _handleServiceResolved(BonsoirService service) { 179 | _resolvedServices[service.name] = service; 180 | 181 | if (kDebugMode) { 182 | print('EasyPasta 设备已解析: ${service.name}'); 183 | print('解析信息: ${service.toJson()}'); 184 | } 185 | 186 | onServicesResolved?.call(_resolvedServices.values.toList()); 187 | } 188 | 189 | /// 处理丢失的服务 190 | void _handleServiceLost(BonsoirService service) { 191 | _discoveredServices.remove(service.name); 192 | _resolvedServices.remove(service.name); 193 | 194 | if (kDebugMode) { 195 | print('EasyPasta 设备离线: ${service.name}'); 196 | } 197 | 198 | onServiceLost?.call(service); 199 | onServicesFound?.call(_discoveredServices.values.toList()); 200 | onServicesResolved?.call(_resolvedServices.values.toList()); 201 | } 202 | 203 | /// 手动解析指定服务(当用户想要连接到该服务时调用) 204 | Future resolveService(String serviceName) async { 205 | final service = _discoveredServices[serviceName]; 206 | if (service != null && _discovery != null) { 207 | try { 208 | await service.resolve(_discovery!.serviceResolver); 209 | if (kDebugMode) { 210 | print('开始解析服务: $serviceName'); 211 | } 212 | } catch (e) { 213 | final error = '解析服务失败: $e'; 214 | if (kDebugMode) print(error); 215 | onError?.call(error); 216 | } 217 | } 218 | } 219 | 220 | /// 获取当前发现的服务列表(未解析) 221 | List get discoveredServices => 222 | _discoveredServices.values.toList(); 223 | 224 | /// 获取当前已解析的服务列表 225 | List get resolvedServices => 226 | _resolvedServices.values.toList(); 227 | 228 | /// 检查服务是否正在运行 229 | bool get isServiceRunning => _broadcast != null; 230 | 231 | /// 检查是否正在发现设备 232 | bool get isDiscovering => _discovery != null; 233 | 234 | /// 根据服务名获取发现的服务 235 | BonsoirService? getDiscoveredService(String name) { 236 | return _discoveredServices[name]; 237 | } 238 | 239 | /// 根据服务名获取已解析的服务 240 | BonsoirService? getResolvedService(String name) { 241 | return _resolvedServices[name]; 242 | } 243 | 244 | /// 获取当前广播的服务信息 245 | BonsoirService? get currentService => _service; 246 | 247 | /// 清理资源 248 | Future dispose() async { 249 | await stopService(); 250 | await stopDiscovery(); 251 | onServicesFound = null; 252 | onServicesResolved = null; 253 | onServiceLost = null; 254 | onError = null; 255 | onServiceStateChanged = null; 256 | onDiscoveryStateChanged = null; 257 | } 258 | } 259 | 260 | /// 线程安全的同步方法 261 | void synchronized(Object lock, void Function() body) { 262 | // Flutter 中的简单同步实现 263 | body(); 264 | } -------------------------------------------------------------------------------- /macos/Runner/WindowUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainUtils.swift 3 | // Runner 4 | // 5 | // Created by Harlans on 2023/5/25. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSWorkspace { 11 | /** 12 | Get an app name from an app URL. 13 | 14 | ``` 15 | let app = WindowInfo.appOwningFrontmostWindow() 16 | app?.url 17 | NSWorkspace.shared.appName(for: …) 18 | //=> "Lungo" 19 | ``` 20 | */ 21 | func appName(for url: URL) -> String { 22 | url.localizedName.removingSuffix(".app") 23 | } 24 | } 25 | 26 | 27 | extension NSRunningApplication { 28 | /** 29 | Like `.localizedName` but guaranteed to return something useful even if the name is not available. 30 | */ 31 | var localizedTitle: String { 32 | localizedName 33 | ?? executableURL?.deletingPathExtension().lastPathComponent 34 | ?? bundleURL?.deletingPathExtension().lastPathComponent 35 | ?? bundleIdentifier 36 | ?? (processIdentifier == -1 ? nil : "PID\(processIdentifier)") 37 | ?? "" 38 | } 39 | } 40 | 41 | extension BinaryInteger { 42 | var boolValue: Bool { self != 0 } 43 | } 44 | 45 | 46 | /** 47 | Static representation of a window. 48 | 49 | - Note: The `name` property is always `nil` on macOS 10.15 and later unless you request “Screen Recording” permission. 50 | */ 51 | struct WindowInfo { 52 | struct Owner { 53 | let name: String 54 | let processIdentifier: Int 55 | let bundleIdentifier: String? 56 | let app: NSRunningApplication? 57 | } 58 | 59 | // Most of these keys are guaranteed to exist: https://developer.apple.com/documentation/coregraphics/quartz_window_services/required_window_list_keys 60 | 61 | let identifier: CGWindowID 62 | let name: String? 63 | let owner: Owner 64 | let bounds: CGRect 65 | let layer: Int 66 | let alpha: Double 67 | let memoryUsage: Int 68 | let sharingState: CGWindowSharingType // https://stackoverflow.com/questions/27695742/what-does-kcgwindowsharingstate-actually-do 69 | let isOnScreen: Bool 70 | 71 | /** 72 | Accepts a window dictionary coming from `CGWindowListCopyWindowInfo`. 73 | */ 74 | private init(windowDictionary window: [String: Any]) { 75 | self.identifier = window[kCGWindowNumber as String] as! CGWindowID 76 | self.name = window[kCGWindowName as String] as? String 77 | 78 | let processIdentifier = window[kCGWindowOwnerPID as String] as! Int 79 | let app = NSRunningApplication(processIdentifier: pid_t(processIdentifier)) 80 | 81 | self.owner = Owner( 82 | name: window[kCGWindowOwnerName as String] as? String ?? app?.localizedTitle ?? "", 83 | processIdentifier: processIdentifier, 84 | bundleIdentifier: app?.bundleIdentifier, 85 | app: app 86 | ) 87 | 88 | self.bounds = CGRect(dictionaryRepresentation: window[kCGWindowBounds as String] as! CFDictionary)! 89 | self.layer = window[kCGWindowLayer as String] as! Int 90 | self.alpha = window[kCGWindowAlpha as String] as! Double 91 | self.memoryUsage = window[kCGWindowMemoryUsage as String] as? Int ?? 0 92 | self.sharingState = CGWindowSharingType(rawValue: window[kCGWindowSharingState as String] as! UInt32)! 93 | self.isOnScreen = (window[kCGWindowIsOnscreen as String] as? Int)?.boolValue ?? false 94 | } 95 | } 96 | 97 | 98 | extension WindowInfo { 99 | typealias Filter = (Self) -> Bool 100 | 101 | /** 102 | Filters out fully transparent windows and windows smaller than 50 width or height. 103 | */ 104 | static func defaultFilter(window: Self) -> Bool { 105 | let minimumWindowSize = 50.0 106 | 107 | // Skip windows outside the expected level range. 108 | guard 109 | window.layer < NSWindow.Level.screenSaver.rawValue, 110 | window.layer >= NSWindow.Level.normal.rawValue 111 | else { 112 | return false 113 | } 114 | 115 | // Skip fully transparent windows, like with Chrome. 116 | guard window.alpha > 0 else { 117 | return false 118 | } 119 | 120 | // Skip tiny windows, like the Chrome link hover statusbar. 121 | guard 122 | window.bounds.width >= minimumWindowSize, 123 | window.bounds.height >= minimumWindowSize 124 | else { 125 | return false 126 | } 127 | 128 | // You might think that we could simply skip windows that are `window.owner.app?.activationPolicy != .regular`, but menu bar apps are `.accessory`, and they might be the source of some copied data. 129 | guard !window.owner.name.lowercased().hasSuffix("agent") else { 130 | return false 131 | } 132 | 133 | let appIgnoreList = [ 134 | "com.apple.dock", 135 | "com.apple.notificationcenterui", 136 | "com.apple.screencaptureui", 137 | "com.apple.PIPAgent", 138 | "com.sindresorhus.Pasteboard-Viewer" 139 | ] 140 | 141 | if appIgnoreList.contains(window.owner.bundleIdentifier ?? "") { 142 | return false 143 | } 144 | 145 | return true 146 | } 147 | 148 | static func allWindows( 149 | options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements], 150 | filter: Filter = defaultFilter 151 | ) -> [Self] { 152 | let info = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? [] 153 | return info.map { self.init(windowDictionary: $0) }.filter(filter) 154 | } 155 | } 156 | 157 | struct UserApp: Hashable, Identifiable { 158 | let url: URL 159 | let bundleIdentifier: String 160 | 161 | var id: URL { url } 162 | } 163 | 164 | 165 | extension WindowInfo { 166 | /** 167 | Returns the URL and bundle identifier of the app that owns the frontmost window. 168 | 169 | This method returns more correct results than `NSWorkspace.shared.frontmostApplication?.bundleIdentifier`. For example, the latter cannot correctly detect the 1Password Mini window. 170 | */ 171 | static func appOwningFrontmostWindow() -> UserApp? { 172 | func createApp(_ runningApp: NSRunningApplication?) -> UserApp? { 173 | guard 174 | let runningApp, 175 | let url = runningApp.bundleURL, 176 | let bundleIdentifier = runningApp.bundleIdentifier 177 | else { 178 | return nil 179 | } 180 | 181 | return UserApp(url: url, bundleIdentifier: bundleIdentifier) 182 | } 183 | 184 | guard 185 | let app = ( 186 | allWindows() 187 | // TODO: Use `.firstNonNil()` here when available. 188 | .lazy 189 | .compactMap { createApp($0.owner.app) } 190 | .first 191 | ) 192 | else { 193 | return createApp(NSWorkspace.shared.frontmostApplication) 194 | } 195 | 196 | return app 197 | } 198 | } 199 | 200 | extension URL { 201 | private func resourceValue(forKey key: URLResourceKey) -> T? { 202 | guard let values = try? resourceValues(forKeys: [key]) else { 203 | return nil 204 | } 205 | 206 | return values.allValues[key] as? T 207 | } 208 | 209 | var localizedName: String { resourceValue(forKey: .localizedNameKey) ?? lastPathComponent } 210 | } 211 | 212 | extension String { 213 | func removingSuffix(_ suffix: Self, caseSensitive: Bool = true) -> Self { 214 | guard caseSensitive else { 215 | guard let range = range(of: suffix, options: [.caseInsensitive, .anchored, .backwards]) else { 216 | return self 217 | } 218 | 219 | return replacingCharacters(in: range, with: "") 220 | } 221 | 222 | guard hasSuffix(suffix) else { 223 | return self 224 | } 225 | 226 | return Self(dropLast(suffix.count)) 227 | } 228 | } 229 | --------------------------------------------------------------------------------