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

428 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 'dart:typed_data';
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) {
// Phase 4: Watch new 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) => Center(child: Text('エラーが発生しました: $error')),
),
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) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('共有エラー: $e')),
);
}
}
}
/// 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] 📤 アップロード開始: $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] ❌ アップロード失敗: ID取得不可');
throw Exception('アップロードに失敗しましたIDなし');
}
debugPrint('[PDF_DRIVE] ✅ アップロード完了: 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) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Driveアップロードエラー: $e'),
duration: const 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) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('印刷エラー: $e')),
);
}
}
}
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,
),
],
),
),
);
}
}