From ac54bee9f13dd6ce1550898f38bc534f698deffa Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 31 Jan 2026 15:08:34 +0900 Subject: [PATCH] gemini nextcloud --- .../android/app/src/main/AndroidManifest.xml | 2 + gemi_invoice/lib/main.dart | 156 +++++++++++------ .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + gemi_invoice/pubspec.lock | 157 ++++++++++++++++++ gemi_invoice/pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 9 + .../windows/flutter/generated_plugins.cmake | 3 + 9 files changed, 289 insertions(+), 48 deletions(-) diff --git a/gemi_invoice/android/app/src/main/AndroidManifest.xml b/gemi_invoice/android/app/src/main/AndroidManifest.xml index 92bfc2c..b0685e1 100644 --- a/gemi_invoice/android/app/src/main/AndroidManifest.xml +++ b/gemi_invoice/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + { - final _clientController = TextEditingController(text: "現場太郎 様"); - final _amountController = TextEditingController(text: "1500"); - String _status = "フォントを配置してPDFを発行してください"; + final _clientController = TextEditingController(text: "佐々木製作所"); + final _amountController = TextEditingController(text: "250000"); + String _status = "内容を入力してPDFを発行してください"; + // 【設定】ファイル名のフォーマット定数 + static const String filenameTemplate = "{date}(請求){name}_{amount}円_{hash}.pdf"; + + // --- 電話帳から選ぶ --- + Future _pickContact() async { + try { + var status = await Permission.contacts.request(); + if (status.isGranted) { + final contact = await FlutterContacts.openExternalPick(); + if (contact != null) { + final fullContact = await FlutterContacts.getContact(contact.id); + setState(() { + String name = fullContact?.displayName ?? "名称不明"; + _clientController.text = name; + _status = "連絡先から「$name」を読み込みました"; + }); + } + } else if (status.isPermanentlyDenied) { + openAppSettings(); + } + } catch (e) { + setState(() => _status = "エラー: $e"); + } + } + + // --- PDF発行 & 共有ロジック --- Future _generateInvoice() async { final pdf = pw.Document(); - // 1. 日本語フォントの読み込み (assets/fonts/ に配置済み前提) - // もしまだ配置してなければ、この3行をコメントアウトしてください + // フォント読み込み final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf"); final ttf = pw.Font.ttf(fontData); final myTheme = pw.ThemeData.withFont(base: ttf, bold: ttf); @@ -34,29 +64,30 @@ class _InvoiceAppState extends State { final int unitPrice = int.tryParse(_amountController.text) ?? 0; final int tax = (unitPrice * 0.1).floor(); final int total = unitPrice + tax; - final dateStr = DateFormat('yyyy/MM/dd HH:mm').format(DateTime.now()); - // PDFドキュメントの作成 + final now = DateTime.now(); + final dateStr = DateFormat('yyyy/MM/dd HH:mm').format(now); + final dateFileStr = DateFormat('yyyyMMdd').format(now); + final amountFormatter = NumberFormat("#,###"); + + // PDFレイアウト作成 pdf.addPage( pw.Page( - theme: myTheme, // 日本語テーマ適用 + theme: myTheme, build: (context) => pw.Column( - crossAxisAlignment: - pw.CrossAxisAlignment.start, // 'cross' を 'crossAxisAlignment' に修正 + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text("請求書 (Invoice)", style: pw.TextStyle(fontSize: 30)), pw.Divider(), - pw.Text("宛名: $clientName"), + pw.Text("宛名: $clientName 様"), pw.Text("日付: $dateStr"), pw.SizedBox(height: 20), - pw.Text("単価(税抜): $unitPrice 円"), - pw.Text("消費税 (10%): $tax 円"), - pw.Text( - "合計金額(税込): $total 円", - style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 20), - ), + pw.Text("単価(税抜): ${amountFormatter.format(unitPrice)} 円"), + pw.Text("消費税 (10%): ${amountFormatter.format(tax)} 円"), + pw.Text("合計金額(税込): ${amountFormatter.format(total)} 円", + style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 20)), pw.SizedBox(height: 40), - pw.Text("※本書類はシステムにより自動生成され、改ざん防止ハッシュが付与されています。"), + pw.Text("※本証憑はSHA-256ハッシュにより改ざん耐性を担保しています。"), ], ), ), @@ -64,54 +95,83 @@ class _InvoiceAppState extends State { final Uint8List bytes = await pdf.save(); - // 2. SHA-256ハッシュ計算 - final hash = sha256.convert(bytes).toString().substring(0, 10); - final fileName = - "${DateFormat('yyyyMMdd').format(DateTime.now())}_${total}円_hash_$hash.pdf"; + // SHA-256ハッシュ計算(末尾8文字) + final String fullHash = sha256.convert(bytes).toString(); + final String hash = fullHash.substring(fullHash.length - 8); - // 3. 保存 - final directory = await getExternalStorageDirectory(); - final file = File("${directory!.path}/$fileName"); + // ファイル名生成 + String fileName = filenameTemplate + .replaceAll("{date}", dateFileStr) + .replaceAll("{name}", clientName) + .replaceAll("{amount}", amountFormatter.format(total)) + .replaceAll("{hash}", hash); + + // 一時保存 + final directory = await getTemporaryDirectory(); // 共有用なので一時フォルダでOK + final file = File("${directory.path}/$fileName"); await file.writeAsBytes(bytes); + // 【新機能】共有メニューを起動! + await Share.shareXFiles( + [XFile(file.path)], + text: '請求書を送付します: $fileName', + subject: '請求書: $clientName 様' + ); + setState(() { - _status = "【発行成功】\nファイル: $fileName\nハッシュ: $hash"; + _status = "【発行&共有】\n$fileName\nHASH: $hash"; }); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("現場のじぇみエモン請求書 V1.0")), + appBar: AppBar( + title: const Text("現場のじぇみエモン請求書 V1.2"), + backgroundColor: Colors.blueGrey, + ), body: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - TextField( - controller: _clientController, - decoration: const InputDecoration(labelText: "取引先名"), - ), + 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.contact_mail, color: Colors.blue, size: 36), + onPressed: _pickContact, + ), + ]), + const SizedBox(height: 16), TextField( controller: _amountController, - decoration: const InputDecoration(labelText: "単価(10%抜き)"), + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: "単価 (税抜)", border: OutlineInputBorder()) ), - const SizedBox(height: 20), - ElevatedButton( + const SizedBox(height: 24), + ElevatedButton.icon( onPressed: _generateInvoice, + icon: const Icon(Icons.send), + label: const Text("PDF発行 & 共有 (Nextcloudへ)"), style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 50), - ), - child: const Text("日本語PDF発行(ハッシュ付与)"), - ), - const SizedBox(height: 20), - SelectableText( - _status, - style: const TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, + minimumSize: const Size(double.infinity, 60), + backgroundColor: Colors.blueAccent, + foregroundColor: Colors.white, ), ), - ], + const SizedBox(height: 24), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + color: Colors.grey[200], + child: SelectableText(_status, style: const TextStyle(fontFamily: 'monospace')), + ), + ]), ), ), ); diff --git a/gemi_invoice/linux/flutter/generated_plugin_registrant.cc b/gemi_invoice/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/gemi_invoice/linux/flutter/generated_plugin_registrant.cc +++ b/gemi_invoice/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/gemi_invoice/linux/flutter/generated_plugins.cmake b/gemi_invoice/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/gemi_invoice/linux/flutter/generated_plugins.cmake +++ b/gemi_invoice/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/gemi_invoice/macos/Flutter/GeneratedPluginRegistrant.swift b/gemi_invoice/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..df11fd8 100644 --- a/gemi_invoice/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/gemi_invoice/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import share_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) } diff --git a/gemi_invoice/pubspec.lock b/gemi_invoice/pubspec.lock index e35462e..1f1e9ac 100644 --- a/gemi_invoice/pubspec.lock +++ b/gemi_invoice/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: "direct main" description: @@ -113,11 +121,27 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_contacts: + dependency: "direct main" + description: + name: flutter_contacts + sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" flutter_lints: dependency: "direct dev" description: @@ -131,6 +155,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -227,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" native_toolchain_c: dependency: transitive description: @@ -315,6 +352,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.11.3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -363,6 +448,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" sky_engine: dependency: transitive description: flutter @@ -424,6 +525,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_math: dependency: transitive description: @@ -440,6 +581,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: diff --git a/gemi_invoice/pubspec.yaml b/gemi_invoice/pubspec.yaml index deb2412..0bca8bd 100644 --- a/gemi_invoice/pubspec.yaml +++ b/gemi_invoice/pubspec.yaml @@ -38,6 +38,9 @@ dependencies: path_provider: ^2.1.5 crypto: ^3.0.7 intl: ^0.20.2 + flutter_contacts: ^1.1.9+2 + permission_handler: ^12.0.1 + share_plus: ^12.0.1 dev_dependencies: flutter_test: diff --git a/gemi_invoice/windows/flutter/generated_plugin_registrant.cc b/gemi_invoice/windows/flutter/generated_plugin_registrant.cc index 8b6d468..d5013ba 100644 --- a/gemi_invoice/windows/flutter/generated_plugin_registrant.cc +++ b/gemi_invoice/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,15 @@ #include "generated_plugin_registrant.h" +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/gemi_invoice/windows/flutter/generated_plugins.cmake b/gemi_invoice/windows/flutter/generated_plugins.cmake index b93c4c3..a0d1388 100644 --- a/gemi_invoice/windows/flutter/generated_plugins.cmake +++ b/gemi_invoice/windows/flutter/generated_plugins.cmake @@ -3,6 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST