xworkmate-app/lib/features/assistant/assistant_page_composer_clipboard.dart
Haitao Pan 6fb1441226
refactor: replace super_clipboard with pasteboard (drop cargokit/Rust) (#55)
* refactor: replace super_clipboard with pasteboard, drop cargokit/Rust

super_clipboard pulled in super_native_extensions (a Rust native layer
built via cargokit), whose precompiled-binary download from GitHub
release assets has been intermittently failing the build ("Connection
closed while receiving data"). It was used for exactly one feature -
reading a clipboard image into the composer - in a single file; the
other 12 imports were dead.

- Swap super_clipboard -> pasteboard (platform-channel, no Rust).
- Rewrite readClipboardImageAsXFileInternal() on Pasteboard.image
  (PNG bytes), collapsing three helpers into one.
- Remove 12 unused super_clipboard imports.
- Regenerated plugin registrants / lockfiles drop super_native_extensions.

Removes the Rust toolchain requirement and the flaky download entirely.
Text copy/paste already used Flutter's built-in Clipboard and is
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci: keep TestFlight package release-only

---------

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:30:26 +08:00

118 lines
3.9 KiB
Dart

// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:pasteboard/pasteboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/assistant_focus_panel.dart';
import '../../widgets/assistant_artifact_sidebar.dart';
import '../../widgets/desktop_workspace_scaffold.dart';
import '../../widgets/pane_resize_handle.dart';
import '../../widgets/surface_card.dart';
import 'assistant_page_main.dart';
import 'assistant_page_components.dart';
import 'assistant_page_composer_bar.dart';
import 'assistant_page_composer_state_helpers.dart';
import 'assistant_page_composer_support.dart';
import 'assistant_page_tooltip_labels.dart';
import 'assistant_page_message_widgets.dart';
import 'assistant_page_task_models.dart';
import 'assistant_page_composer_skill_picker.dart';
import 'assistant_page_components_core.dart';
class ComposerAttachmentInternal {
const ComposerAttachmentInternal({
required this.name,
required this.path,
required this.icon,
required this.mimeType,
});
final String name;
final String path;
final IconData icon;
final String mimeType;
factory ComposerAttachmentInternal.fromXFile(XFile file) {
final extension = file.name.split('.').last.toLowerCase();
final mimeType = switch (extension) {
'png' => 'image/png',
'jpg' || 'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'json' => 'application/json',
'csv' => 'text/csv',
'txt' || 'log' || 'md' || 'yaml' || 'yml' => 'text/plain',
'pdf' => 'application/pdf',
'zip' => 'application/zip',
_ => 'application/octet-stream',
};
final icon = switch (extension) {
'png' || 'jpg' || 'jpeg' || 'gif' || 'webp' => Icons.image_outlined,
'log' || 'txt' || 'json' || 'csv' => Icons.description_outlined,
_ => Icons.insert_drive_file_outlined,
};
return ComposerAttachmentInternal(
name: file.name,
path: file.path,
icon: icon,
mimeType: mimeType,
);
}
}
class AssistantPasteIntent extends Intent {
const AssistantPasteIntent();
}
Future<XFile?> readClipboardImageAsXFileInternal() async {
// pasteboard normalizes clipboard images to PNG bytes across platforms.
Uint8List? bytes;
try {
bytes = await Pasteboard.image;
} catch (error, stackTrace) {
debugPrint('Error reading clipboard image: $error\n$stackTrace');
return null;
}
if (bytes == null || bytes.isEmpty) {
return null;
}
final temporaryDirectory =
await resolveClipboardAttachmentTempDirectoryInternal();
final fileName =
'clipboard-image-${DateTime.now().microsecondsSinceEpoch}.png';
final file = File('${temporaryDirectory.path}/$fileName');
await file.writeAsBytes(bytes, flush: true);
return XFile(file.path, mimeType: 'image/png', name: fileName);
}
Future<Directory> resolveClipboardAttachmentTempDirectoryInternal() async {
Directory rootDirectory;
try {
rootDirectory = await getTemporaryDirectory();
} catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace');
rootDirectory = Directory.systemTemp;
}
final clipboardDirectory = Directory(
'${rootDirectory.path}/xworkmate-clipboard-attachments',
);
await clipboardDirectory.create(recursive: true);
return clipboardDirectory;
}