ponshu-room-lite/lib/screens/pdf_preview_screen.dart

430 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // debugPrint
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:printing/printing.dart';
import 'package:pdf/pdf.dart';
import 'package:googleapis/drive/v3.dart' as drive;
import 'package:google_sign_in/google_sign_in.dart' as sign_in;
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import '../services/pdf_service.dart';
import '../models/sake_item.dart';
import '../providers/menu_providers.dart';
import '../theme/app_colors.dart';
class PdfPreviewScreen extends ConsumerWidget {
final List<SakeItem> items;
final String title;
final String date;
final bool includePhoto;
final bool includePoem;
final bool includeChart;
final bool includePrice;
final bool includeDate;
final bool includeQr;
final String pdfSize;
final bool isMonochrome;
const PdfPreviewScreen({
super.key,
required this.items,
required this.title,
required this.date,
required this.includePhoto,
required this.includePoem,
required this.includeChart,
required this.includePrice,
required this.includeDate,
required this.includeQr,
required this.pdfSize,
required this.isMonochrome,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// PDF Settings を監視
final isPortrait = ref.watch(pdfIsPortraitProvider);
final density = ref.watch(pdfDensityProvider);
return Scaffold(
backgroundColor: Colors.white, // 明示的に白背景を設定
appBar: AppBar(
title: const Text('お品書きプレビュー'),
automaticallyImplyLeading: false, // ヘッダー戻るボタンを無効化
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _showExitDialog(context, ref),
tooltip: '終了',
),
],
),
body: PdfPreview(
build: (format) => PdfService.generateMenuPdf(
items,
title: title,
date: date,
includePhoto: includePhoto,
includePoem: includePoem,
includeChart: includeChart,
includePrice: includePrice,
includeDate: includeDate,
includeQr: includeQr,
pdfSize: pdfSize,
isMonochrome: isMonochrome,
isPortrait: isPortrait,
density: density,
),
initialPageFormat: _getPageFormat(pdfSize, isPortrait),
canChangePageFormat: false, // User selects in Edit screen
canChangeOrientation: false,
canDebug: false,
allowSharing: false, // Handled by custom button
allowPrinting: false, // Handled by custom button if needed (or share -> print)
useActions: false, // Disable default bottom bar
scrollViewDecoration: const BoxDecoration(
color: Colors.white,
),
pdfPreviewPageDecoration: BoxDecoration(
color: Colors.white, // FIX: Ensure paper is white
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 2,
),
],
),
loadingWidget: const Center(child: CircularProgressIndicator()),
onError: (context, error) => const Center(child: Text('PDFの表示に失敗しました')),
),
bottomNavigationBar: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 操作ガイド (フッターの真上)
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.touch_app, color: Colors.white, size: 16),
const SizedBox(width: 8),
Flexible(
child: Text(
'メニュー領域を2回タップで拡大・縮小',
style: TextStyle(color: Colors.white, fontSize: 12),
textAlign: TextAlign.center,
),
),
],
),
),
),
),
// フッターボタン
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
// 戻るボタン (左端)
SizedBox(
width: 56,
height: 56,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: Colors.grey[300]!),
padding: EdgeInsets.zero,
),
child: Icon(Icons.arrow_back, color: Colors.grey[700]),
),
),
const SizedBox(width: 8),
// 共有ボタン
Expanded(
child: _PdfActionButton(
icon: Icons.share,
label: '共有',
color: Theme.of(context).extension<AppColors>()!.brandPrimary,
onPressed: () => _sharePdf(context, ref),
),
),
const SizedBox(width: 8),
// Google Driveボタン
Expanded(
child: _PdfActionButton(
icon: Icons.cloud_upload,
label: 'Drive',
color: Theme.of(context).extension<AppColors>()!.brandAccent,
onPressed: () => _uploadToDrive(context, ref),
),
),
const SizedBox(width: 8),
// 印刷ボタン
Expanded(
child: _PdfActionButton(
icon: Icons.print,
label: '印刷',
color: Theme.of(context).extension<AppColors>()!.textSecondary,
onPressed: () => _printPdf(context, ref),
),
),
],
),
),
],
),
),
);
}
PdfPageFormat _getPageFormat(String size, bool isPortrait) {
PdfPageFormat format;
switch (size) {
case 'a5':
format = PdfPageFormat.a5; break;
case 'b5':
format = PdfPageFormat(257 * PdfPageFormat.mm, 182 * PdfPageFormat.mm); break;
case 'a4':
default:
format = PdfPageFormat.a4; break;
}
return isPortrait ? format : format.landscape;
}
/// PDF生成の共通処理
Future<Uint8List> _generatePdfBytes(WidgetRef ref) async {
final isPortrait = ref.read(pdfIsPortraitProvider);
final density = ref.read(pdfDensityProvider);
return await PdfService.generateMenuPdf(
items,
title: title,
date: date,
includePhoto: includePhoto,
includePoem: includePoem,
includeChart: includeChart,
includePrice: includePrice,
includeDate: includeDate,
includeQr: includeQr,
pdfSize: pdfSize,
isMonochrome: isMonochrome,
isPortrait: isPortrait,
density: density,
);
}
/// 共有機能(既存機能)
Future<void> _sharePdf(BuildContext context, WidgetRef ref) async {
try {
final bytes = await _generatePdfBytes(ref);
await Printing.sharePdf(
bytes: bytes,
filename: 'oshinagaki_${DateTime.now().toString().split(' ')[0]}.pdf',
);
} catch (e) {
debugPrint('PDF share error: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFの共有に失敗しました。再度お試しください。')),
);
}
}
}
/// Google Driveアップロード機能
Future<void> _uploadToDrive(BuildContext context, WidgetRef ref) async {
try {
// 1. Google Sign In with Drive scope
final googleSignIn = sign_in.GoogleSignIn(
scopes: [drive.DriveApi.driveFileScope],
);
final account = await googleSignIn.signIn();
if (account == null) return; // ユーザーがキャンセル
// 2. Get authenticated HTTP client
final httpClient = (await googleSignIn.authenticatedClient())!;
final driveApi = drive.DriveApi(httpClient);
// 3. Generate PDF
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFを生成中...')),
);
}
final bytes = await _generatePdfBytes(ref);
// 4. Upload to Drive
final fileName = 'お品書き_${DateTime.now().toString().split(' ')[0]}.pdf';
final driveFile = drive.File()
..name = fileName
..mimeType = 'application/pdf';
debugPrint('[PDF_DRIVE] Upload start: $fileName (${bytes.length} bytes)');
final uploadedFile = await driveApi.files.create(
driveFile,
uploadMedia: drive.Media(
Stream.value(bytes.toList()),
bytes.length,
),
);
if (uploadedFile.id == null) {
debugPrint('[PDF_DRIVE] Upload failed: no file ID returned');
throw Exception('アップロードに失敗しましたIDなし');
}
debugPrint('[PDF_DRIVE] Upload complete: ID=${uploadedFile.id}');
// 5. Success notification
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Google Driveに保存しました: $fileName'),
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'OK',
onPressed: () {},
),
),
);
}
} catch (e) {
debugPrint('Drive upload error: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Google Driveへの保存に失敗しました。再度お試しください。'),
duration: Duration(seconds: 4),
),
);
}
}
}
/// 印刷機能
Future<void> _printPdf(BuildContext context, WidgetRef ref) async {
try {
final bytes = await _generatePdfBytes(ref);
await Printing.layoutPdf(
onLayout: (_) => bytes,
name: 'お品書き',
format: _getPageFormat(pdfSize, ref.read(pdfIsPortraitProvider)),
);
} catch (e) {
debugPrint('Print error: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('印刷に失敗しました。再度お試しください。')),
);
}
}
}
Future<void> _showExitDialog(BuildContext context, WidgetRef ref) async {
final appColors = Theme.of(context).extension<AppColors>()!;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('お品書き作成を終了しますか?'),
content: const Text('入力内容は保存されません。'),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () => Navigator.pop(context, false),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: appColors.brandPrimary,
foregroundColor: appColors.surfaceSubtle,
),
onPressed: () => Navigator.pop(context, true),
child: const Text('終了'),
),
],
),
);
if (confirmed == true && context.mounted) {
ref.read(menuModeProvider.notifier).set(false);
ref.read(selectedMenuSakeIdsProvider.notifier).clear();
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
}
/// PDF操作ボタンの共通ウィジェット
class _PdfActionButton extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
final VoidCallback onPressed;
const _PdfActionButton({
required this.icon,
required this.label,
required this.color,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 56,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white, // Always white text on colored buttons
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
elevation: 2, // Add elevation for better visibility
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 20),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}