moussa_pdf 0.1.0+8
moussa_pdf: ^0.1.0+8 copied to clipboard
A secure, native PDF viewer Flutter plugin with drawing tools and snipping support for education workflows.
example/lib/main.dart
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:moussa_pdf/moussa_pdf.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'moussa_pdf example',
theme: ThemeData.dark(),
home: const PdfExampleScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class PdfExampleScreen extends StatefulWidget {
const PdfExampleScreen({super.key});
@override
State<PdfExampleScreen> createState() => _PdfExampleScreenState();
}
class _PdfExampleScreenState extends State<PdfExampleScreen> {
MoussaPdfController? _controller;
StreamSubscription? _eventSub;
StreamSubscription? _snipSub;
// 👈 حط لينك PDF public (يفضل صغير للتجربة)
final String pdfUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
// 👈 دول أمثلة - عندك في مشروعك هتيجي من اليوزر
final String userId = "user_123";
final String fileId = "file_abc";
String status = "Waiting...";
double? progress; // 0..1
@override
void dispose() {
_snipSub?.cancel();
_eventSub?.cancel();
_controller?.dispose();
super.dispose();
}
Future<void> _open() async {
final c = _controller;
if (c == null) return;
setState(() {
status = "Opening...";
progress = null;
});
await c.openUrl(url: pdfUrl, userId: userId, fileId: fileId);
}
Future<void> _showSnipPreview(BuildContext context, MoussaPdfSnip snip) async {
final bytes = snip.bytes;
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: const Color(0xFF111111),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) {
return Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 16 + MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(10),
),
),
Text(
"Snip captured (page ${snip.page + 1})",
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(bytes, fit: BoxFit.contain),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () async {
final path = await _saveSnipTemp(bytes);
if (mounted) {
Navigator.pop(context);
_toast("Saved temp: $path");
}
},
icon: const Icon(Icons.save_alt),
label: const Text("Save temp"),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
Navigator.pop(context);
await _sendToTeacher(bytes: bytes, page: snip.page);
},
icon: const Icon(Icons.send),
label: const Text("Send"),
),
),
],
),
const SizedBox(height: 8),
],
),
);
},
);
}
Future<String> _saveSnipTemp(Uint8List bytes) async {
final dir = await getTemporaryDirectory();
final file = File("${dir.path}/moussa_snip_${DateTime.now().millisecondsSinceEpoch}.png");
await file.writeAsBytes(bytes, flush: true);
return file.path;
}
Future<void> _sendToTeacher({required Uint8List bytes, required int page}) async {
// 👇 هنا تربط API بتاعك:
// - multipart upload للصورة
// - أو تبعتها Base64
// - أو تعمل OCR وترسل النص
//
// في المثال ده هنمثل الإرسال بس.
setState(() => status = "Sending snip (page ${page + 1}) ...");
await Future.delayed(const Duration(milliseconds: 600)); // simulate
if (!mounted) return;
_toast("Sent (demo). bytes=${bytes.length}");
setState(() => status = "Ready");
}
void _toast(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), duration: const Duration(seconds: 2)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("moussa_pdf example"),
actions: [
IconButton(
tooltip: "Open URL",
onPressed: _open,
icon: const Icon(Icons.picture_as_pdf),
),
],
),
body: Column(
children: [
if (progress != null)
LinearProgressIndicator(value: progress),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
Expanded(child: Text(status, style: const TextStyle(fontSize: 12))),
const SizedBox(width: 8),
OutlinedButton(
onPressed: () => _controller?.setTool(MoussaPdfTool.hand),
child: const Text("Hand"),
),
const SizedBox(width: 6),
OutlinedButton(
onPressed: () => _controller?.setTool(MoussaPdfTool.snip),
child: const Text("Snip"),
),
],
),
),
Expanded(
child: MoussaPdfView(
onCreated: (c) async {
_controller = c;
// Listen to all events (progress/errors/info)
_eventSub = c.events.listen((e) {
// Debug print (اختياري)
// ignore: avoid_print
print("EVENT: ${e.type} ${e.data}");
if (e.type == "downloadProgress") {
final p = e.data["percent"];
final percent = (p is num) ? p.toDouble() : double.tryParse("$p");
if (percent != null) {
setState(() {
progress = (percent / 100.0).clamp(0.0, 1.0);
status = "Downloading... ${percent.toStringAsFixed(0)}%";
});
}
}
if (e.type == "opened") {
setState(() {
progress = null;
status = "Opened ✅";
});
}
if (e.type == "error") {
final msg = e.data["message"]?.toString() ?? "Unknown error";
setState(() {
progress = null;
status = "Error: $msg";
});
}
});
// Listen to snips
_snipSub = c.snips.listen((snip) {
_showSnipPreview(context, snip);
});
// Auto open
await _open();
},
onEvent: (e) {
// لو عايز callbacks منفصلة غير stream
// ignore: avoid_print
// print("onEvent: $e");
},
),
),
],
),
);
}
}