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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
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 |

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