clipshare_clipboard_listener 1.0.4
clipshare_clipboard_listener: ^1.0.4 copied to clipboard
Supports clipboard monitoring on Windows、Android、Linux platforms, with support for text and images. For Android 10+ devices, background clipboard monitoring is supported (requires Shizuku).
import 'dart:io';
import 'dart:math';
import 'package:clipshare_clipboard_listener/clipboard_manager.dart';
import 'package:clipshare_clipboard_listener/enums.dart';
import 'package:clipshare_clipboard_listener/models/clipboard_source.dart';
import 'package:clipshare_clipboard_listener/models/notification_content_config.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:permission_handler/permission_handler.dart';
void main(List<String> args) {
var isMultiWindow = args.firstOrNull == 'multi_window';
if (isMultiWindow) {
runApp(MultiWindow());
} else {
runApp(const MyApp());
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with ClipboardListener, WidgetsBindingObserver {
String? type;
String? content;
ClipboardSource? source;
EnvironmentType env = EnvironmentType.none;
bool isGranted = false;
ClipboardListeningWay way = ClipboardListeningWay.logs;
bool hasAlertWindowPermission = false;
bool hasNotificationPermission = false;
bool hasAccessibilityPermission = false;
@override
void initState() {
super.initState();
clipboardManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
initHotKey();
initMultiWindowEvent();
if (Platform.isAndroid) {
clipboardManager.getCurrentEnvironment().then((env) {
setState(() {
this.env = env;
this.isGranted = env != EnvironmentType.none;
});
});
checkAndroidPermissions();
}
}
Future<void> checkAndroidPermissions() async {
hasAlertWindowPermission = await Permission.systemAlertWindow.isGranted;
hasNotificationPermission = await Permission.notification.isGranted;
hasAccessibilityPermission = await clipboardManager.checkAccessibility();
setState(() {});
}
@override
void dispose() {
super.dispose();
clipboardManager.removeListener(this);
WidgetsBinding.instance.removeObserver(this);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Builder(builder: (context) {
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
const SizedBox(
height: 10,
),
//region Android
Visibility(
visible: Platform.isAndroid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Current environment: ${env.name}, Status: $isGranted'),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
children: [
GestureDetector(
onTap: () {
clipboardManager.requestPermission(EnvironmentType.shizuku);
},
child: const Chip(label: Text("Request Shizuka")),
),
const SizedBox(
width: 10,
),
GestureDetector(
onTap: () {
clipboardManager.requestPermission(EnvironmentType.root);
},
child: const Chip(label: Text("Request Root")),
),
const SizedBox(
width: 10,
),
],
),
),
const Text('Permissions:'),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Wrap(
children: [
RawChip(
label: const Text("Alert Window"),
selected: hasAlertWindowPermission,
onSelected: (_) async {
if (hasAlertWindowPermission) {
return;
}
final result = await Permission.systemAlertWindow.request();
print("result isDenied = ${result.isDenied}");
if (result.isGranted) {
setState(() {
hasAlertWindowPermission = result.isGranted;
});
showSnackBarSuc(context, "SystemAlertWindow granted");
} else {
showSnackBarErr(context, "SystemAlertWindow denied");
}
},
),
const SizedBox(width: 10),
RawChip(
label: const Text("Notification"),
selected: hasNotificationPermission,
onSelected: (_) async {
if (hasNotificationPermission) {
return;
}
final result = await Permission.notification.request();
if (result.isGranted) {
setState(() {
hasNotificationPermission = result.isGranted;
});
showSnackBarSuc(context, "Notification granted");
} else {
showSnackBarErr(context, "Notification denied");
}
},
),
const SizedBox(width: 10),
RawChip(
label: const Text("Accessibility"),
selected: hasAccessibilityPermission,
onSelected: (_) async {
if (hasAccessibilityPermission) {
return;
}
clipboardManager.requestAccessibility();
},
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(vertical: 5),
child: Text("Options:"),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: env == EnvironmentType.shizuku
? () {
startListeningOnAndroid(
context,
env: EnvironmentType.shizuku,
way: way,
);
}
: null,
child: Chip(
label: Text(
"Start listening by Shizuku",
style: env == EnvironmentType.shizuku ? null : const TextStyle(color: Colors.grey),
),
),
),
GestureDetector(
onTap: env == EnvironmentType.root
? () {
startListeningOnAndroid(
context,
env: EnvironmentType.root,
way: way,
);
}
: null,
child: Chip(
label: Text(
"Start listening by Root",
style: env == EnvironmentType.root ? null : const TextStyle(color: Colors.grey),
),
),
),
GestureDetector(
onTap: () {
clipboardManager.stopListening();
showSnackBarSuc(
context,
"Listening stopped",
);
},
child: const Chip(label: Text("Stop listening")),
),
],
),
),
),
Container(
margin: const EdgeInsets.symmetric(vertical: 5),
child: Text("listening way: ${way.name}"),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
children: [
RawChip(
label: const Text("Hidden API"),
selected: way == ClipboardListeningWay.hiddenApi,
onSelected: (_) async {
setState(() {
way = ClipboardListeningWay.hiddenApi;
});
await clipboardManager.stopListening();
Future.delayed(const Duration(seconds: 1), () {
startListeningOnAndroid(
context,
env: env,
way: ClipboardListeningWay.hiddenApi,
);
});
},
),
const SizedBox(width: 10),
RawChip(
label: const Text("System Logs"),
selected: way == ClipboardListeningWay.logs,
onSelected: (_) async {
setState(() {
way = ClipboardListeningWay.logs;
});
await clipboardManager.stopListening();
Future.delayed(const Duration(seconds: 1), () {
startListeningOnAndroid(
context,
env: env,
way: ClipboardListeningWay.logs,
);
});
},
),
],
),
),
],
),
),
//endregion
//region Linux
Visibility(
visible: !Platform.isAndroid,
child: Column(
children: [
GestureDetector(
onTap: () {
clipboardManager.startListening().then((res) {
if (res) {
showSnackBarSuc(
context,
"Listening started successfully",
);
} else {
showSnackBarErr(
context,
"Listening failed to start",
);
}
});
},
child: const Chip(label: Text("Start listening")),
),
GestureDetector(
onTap: () {
clipboardManager.stopListening().then((res) {
showSnackBarSuc(
context,
"Listening stopped successfully",
);
}).catchError((err) {
showSnackBarErr(
context,
"Listening failed to stop",
);
});
},
child: const Chip(label: Text("Stop listening")),
),
],
),
),
//endregion
Text('type: $type\n\ncontent:\n$content\n\nsource:${source?.name}\n\n'),
const SizedBox(height: 10),
if (source?.iconBytes != null)
Image.memory(
source!.iconBytes!,
height: 30,
width: 30,
),
const SizedBox(height: 10),
const TextField(),
const SizedBox(
height: 10,
),
GestureDetector(
onTap: () {
clipboardManager.copy(ClipboardContentType.text, Random().nextInt(99999).toString());
},
child: const Chip(label: Text("Copy Random Data")),
),
GestureDetector(
onTap: () {
clipboardManager.copy(ClipboardContentType.image, "/tmp/2025-01-16_22-29-42-6.png");
},
child: const Chip(label: Text("Copy Test Image(mannal set on code)")),
),
],
),
);
}),
),
);
}
@override
void onClipboardChanged(ClipboardContentType type, String content, ClipboardSource? source) {
print("type: ${type.name}, content: $content");
setState(() {
this.type = type.name;
this.content = content;
});
setState(() {
this.source = source;
});
var start = DateTime.now();
clipboardManager.getLatestWriteClipboardSource().then((source) {
if (source == null) {
return;
}
final isTimeout = source.isTimeout(2000);
print("source time: ${source.time?.toString()}, timeout: $isTimeout");
var end = DateTime.now();
print("source: ${source.name}, offset: ${end.difference(start).inMilliseconds}");
if (isTimeout) {
return;
}
setState(() {
this.source = source;
});
}).catchError((err) {
print("error: $err");
});
}
@override
void onPermissionStatusChanged(EnvironmentType environment, bool isGranted) {
debugPrint("env: ${environment.name}, granted: $isGranted");
setState(() {
env = environment;
this.isGranted = isGranted;
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
if (Platform.isAndroid) {
checkAndroidPermissions();
}
break;
default:
}
}
Future startListeningOnAndroid(
BuildContext context, {
NotificationContentConfig? notificationContentConfig,
EnvironmentType? env,
ClipboardListeningWay? way,
}) {
if (!Platform.isAndroid) return Future.value();
//Android version>= 10
if (!hasAlertWindowPermission) {
showSnackBarErr(context, "No Alert Window permission");
return Future.value();
}
//Android version>= 10
if (!hasNotificationPermission) {
showSnackBarErr(context, "No Notification permission");
return Future.value();
}
return clipboardManager
.startListening(
env: env,
way: way,
notificationContentConfig: notificationContentConfig,
)
.then((res) {
if (res) {
showSnackBarSuc(
context,
"Listening started successfully",
);
} else {
showSnackBarErr(
context,
"Listening failed to start",
);
}
});
}
void showSnackBar(BuildContext context, String text, Color color) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(text),
backgroundColor: color,
),
);
}
void showSnackBarSuc(BuildContext context, String text) {
showSnackBar(context, text, Colors.lightBlue);
}
void showSnackBarErr(BuildContext context, String text) {
showSnackBar(context, text, Colors.redAccent);
}
Future<void> initHotKey() async {
await hotKeyManager.unregisterAll();
final key = HotKey(
key: PhysicalKeyboardKey.keyG,
modifiers: [HotKeyModifier.control, HotKeyModifier.alt],
scope: HotKeyScope.system,
);
await hotKeyManager.register(
key,
keyDownHandler: (hotKey) async {
if (Platform.isWindows) {
await clipboardManager.storeCurrentWindowHwnd();
}
//createWindow里面的参数必须传
final window = await DesktopMultiWindow.createWindow('{}');
window
..setFrame(const Offset(500, 500) & const Size(355.0, 630.0))
..setTitle('Window')
..show();
},
);
}
void initMultiWindowEvent() {
DesktopMultiWindow.setMethodHandler((
MethodCall call,
int fromWindowId,
) {
Clipboard.setData(ClipboardData(text: DateTime.now().toString()));
clipboardManager.pasteToPreviousWindow();
return Future.value();
});
}
}
class MultiWindow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("MultiWindow"),
),
body: Column(
children: [
TextButton(
onPressed: () {
DesktopMultiWindow.invokeMethod(
0,
"methodName",
"{}",
);
},
child: const Text("click me"))
],
),
),
);
}
}