h-1.flutter.0/lib/main_monolithic.dart.bak

219 lines
7.5 KiB
Dart

// version: 1.4.3c (Bug Fix: PDF layout error)
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:share_plus/share_plus.dart';
import 'package:open_filex/open_filex.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart';
void main() => runApp(const MaterialApp(home: InvoiceApp()));
class InvoiceApp extends StatefulWidget {
const InvoiceApp({super.key});
@override
State<InvoiceApp> createState() => _InvoiceAppState();
}
class _InvoiceAppState extends State<InvoiceApp> {
final _clientController = TextEditingController(text: "佐々木製作所");
final _amountController = TextEditingController(text: "250000");
String _status = "内容を入力してPDFを発行してください";
String? _lastFilePath;
Future<void> _pickContact() async {
setState(() => _status = "連絡先をスキャン中...");
try {
if (await FlutterContacts.requestPermission()) {
final List<Contact> contacts = await FlutterContacts.getContacts(
withProperties: false,
withThumbnail: false,
);
if (!mounted) return;
if (contacts.isEmpty) {
setState(() => _status = "連絡先が空、または取得できませんでした。");
return;
}
final Contact? selected = await showDialog<Contact>(
context: context,
builder: (ctx) => AlertDialog(
title: Text("取引先を選択 (${contacts.length}件)"),
content: SizedBox(
width: double.maxFinite,
height: 400,
child: ListView.builder(
itemCount: contacts.length,
itemBuilder: (c, i) => ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(contacts[i].displayName),
onTap: () => Navigator.pop(c, contacts[i]),
),
),
),
),
);
if (selected != null) {
setState(() {
_clientController.text = selected.displayName;
_status = "${selected.displayName}」をセットしました";
});
}
} else {
setState(() => _status = "電話帳の権限が拒否されています。");
}
} catch (e) {
setState(() => _status = "エラーが発生しました: $e");
}
}
Future<void> _methodDirectOpen() async {
if (_lastFilePath != null) {
await OpenFilex.open(_lastFilePath!);
}
}
Future<void> _methodShare() async {
if (_lastFilePath != null) {
await Share.shareXFiles([XFile(_lastFilePath!)], text: '請求書送付');
}
}
Future<void> _generateInvoice() async {
final pdf = pw.Document();
final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData);
final clientName = _clientController.text;
final int unitPrice = int.tryParse(_amountController.text) ?? 0;
final int total = (unitPrice * 1.1).floor();
final now = DateTime.now();
final dateFileStr = DateFormat('yyyyMMdd').format(now);
final amountFormatter = NumberFormat("#,###");
pdf.addPage(pw.Page(
theme: pw.ThemeData.withFont(base: ttf, bold: ttf),
build: (context) => pw.Center(
child: pw.Column(
// --- ここを修正しました ---
mainAxisAlignment: pw.MainAxisAlignment.center,
children: [
pw.Text("請求書", style: pw.TextStyle(fontSize: 24)),
pw.SizedBox(height: 20),
pw.Text("宛名: $clientName"),
pw.Text("合計金額: ${amountFormatter.format(total)} 円 (税込)"),
pw.SizedBox(height: 10),
pw.Text("日付: ${DateFormat('yyyy/MM/dd').format(now)}"),
],
)
),
));
final Uint8List bytes = await pdf.save();
final String hash = sha256.convert(bytes).toString().substring(sha256.convert(bytes).toString().length - 8);
String fileName = "${dateFileStr}(請求)${clientName}_${amountFormatter.format(total)}円_${hash}.pdf";
final directory = await getExternalStorageDirectory();
if (directory == null) return;
final file = File("${directory.path}/$fileName");
await file.writeAsBytes(bytes);
setState(() {
_lastFilePath = file.path;
_status = "【原本保存完了】\n$fileName";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("じぇみエモン V1.4.3c"), backgroundColor: Colors.blueGrey),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(children: [
Row(children: [
Expanded(
child: TextField(
controller: _clientController,
decoration: const InputDecoration(labelText: "取引先名", border: OutlineInputBorder())
)
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.person_search, color: Colors.blue, size: 40),
onPressed: _pickContact,
),
]),
const SizedBox(height: 16),
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: "単価 (税抜)", border: OutlineInputBorder())
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _generateInvoice,
icon: const Icon(Icons.save),
label: const Text("原本PDFをローカル保存"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 60),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white
),
),
const SizedBox(height: 20),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _lastFilePath == null ? null : _methodDirectOpen,
icon: const Icon(Icons.launch),
label: const Text("内容確認(A)"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 50),
backgroundColor: Colors.orange,
foregroundColor: Colors.white
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _lastFilePath == null ? null : _methodShare,
icon: const Icon(Icons.share),
label: const Text("外部共有(B)"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 50),
backgroundColor: Colors.green,
foregroundColor: Colors.white
),
),
),
]),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
color: Colors.grey[100],
child: Text(_status, style: const TextStyle(fontSize: 12, color: Colors.black54)),
),
]),
),
),
);
}
}