gemini nextcloud

This commit is contained in:
joe 2026-01-31 15:08:34 +09:00
parent 63d944a791
commit ac54bee9f1
9 changed files with 289 additions and 48 deletions

View file

@ -1,4 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:label="gemi_invoice"
android:name="${applicationName}"

View file

@ -1,8 +1,13 @@
// version: 1.0.2 (Japanese Support & SHA-256 Edition)
// version: 1.2.0 (Share-ready, Bottom-8 Hash, Template Edition)
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
//
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart'; //
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
@ -17,15 +22,40 @@ class InvoiceApp extends StatefulWidget {
}
class _InvoiceAppState extends State<InvoiceApp> {
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<void> _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<void> _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<InvoiceApp> {
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<InvoiceApp> {
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-2568
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(
child: SingleChildScrollView(
child: Column(children: [
Row(children: [
Expanded(
child: TextField(
controller: _clientController,
decoration: const InputDecoration(labelText: "取引先名"),
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')),
),
]),
),
),
);

View file

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
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);
}

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View file

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import share_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
}

View file

@ -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:

View file

@ -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:

View file

@ -6,6 +6,15 @@
#include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View file

@ -3,6 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
share_plus
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST