h-1.flutter.0/lib/screens/settings_screen.dart
2026-02-27 16:25:27 +09:00

469 lines
19 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 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'company_info_screen.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
// Company
final _companyNameCtrl = TextEditingController();
final _companyZipCtrl = TextEditingController();
final _companyAddrCtrl = TextEditingController();
final _companyTelCtrl = TextEditingController();
final _companyRegCtrl = TextEditingController();
final _companyFaxCtrl = TextEditingController();
final _companyEmailCtrl = TextEditingController();
final _companyUrlCtrl = TextEditingController();
// Staff
final _staffNameCtrl = TextEditingController();
final _staffMailCtrl = TextEditingController();
// SMTP
final _smtpHostCtrl = TextEditingController();
final _smtpPortCtrl = TextEditingController(text: '587');
final _smtpUserCtrl = TextEditingController();
final _smtpPassCtrl = TextEditingController();
final _smtpBccCtrl = TextEditingController();
bool _smtpTls = true;
// External sync (母艦システム「お局様」連携)
final _externalHostCtrl = TextEditingController();
final _externalPassCtrl = TextEditingController();
// Backup
final _backupPathCtrl = TextEditingController();
String _theme = 'system';
// Kana map (kanji -> kana head)
Map<String, String> _customKanaMap = {};
final _kanaKeyCtrl = TextEditingController();
final _kanaValCtrl = TextEditingController();
// SharedPreferences keys
static const _kCompanyName = 'company_name';
static const _kCompanyZip = 'company_zip';
static const _kCompanyAddr = 'company_addr';
static const _kCompanyTel = 'company_tel';
static const _kCompanyReg = 'company_reg';
static const _kCompanyFax = 'company_fax';
static const _kCompanyEmail = 'company_email';
static const _kCompanyUrl = 'company_url';
static const _kStaffName = 'staff_name';
static const _kStaffMail = 'staff_mail';
static const _kSmtpHost = 'smtp_host';
static const _kSmtpPort = 'smtp_port';
static const _kSmtpUser = 'smtp_user';
static const _kSmtpPass = 'smtp_pass';
static const _kSmtpTls = 'smtp_tls';
static const _kSmtpBcc = 'smtp_bcc';
static const _kExternalHost = 'external_host';
static const _kExternalPass = 'external_pass';
static const _kCryptKey = 'test';
static const _kBackupPath = 'backup_path';
@override
void dispose() {
_companyNameCtrl.dispose();
_companyZipCtrl.dispose();
_companyAddrCtrl.dispose();
_companyTelCtrl.dispose();
_companyRegCtrl.dispose();
_companyFaxCtrl.dispose();
_companyEmailCtrl.dispose();
_companyUrlCtrl.dispose();
_staffNameCtrl.dispose();
_staffMailCtrl.dispose();
_smtpHostCtrl.dispose();
_smtpPortCtrl.dispose();
_smtpUserCtrl.dispose();
_smtpPassCtrl.dispose();
_smtpBccCtrl.dispose();
_externalHostCtrl.dispose();
_externalPassCtrl.dispose();
_backupPathCtrl.dispose();
_kanaKeyCtrl.dispose();
_kanaValCtrl.dispose();
super.dispose();
}
Future<void> _loadAll() async {
await _loadKanaMap();
final prefs = await SharedPreferences.getInstance();
setState(() {
_companyNameCtrl.text = prefs.getString(_kCompanyName) ?? '';
_companyZipCtrl.text = prefs.getString(_kCompanyZip) ?? '';
_companyAddrCtrl.text = prefs.getString(_kCompanyAddr) ?? '';
_companyTelCtrl.text = prefs.getString(_kCompanyTel) ?? '';
_companyRegCtrl.text = prefs.getString(_kCompanyReg) ?? '';
_companyFaxCtrl.text = prefs.getString(_kCompanyFax) ?? '';
_companyEmailCtrl.text = prefs.getString(_kCompanyEmail) ?? '';
_companyUrlCtrl.text = prefs.getString(_kCompanyUrl) ?? '';
_staffNameCtrl.text = prefs.getString(_kStaffName) ?? '';
_staffMailCtrl.text = prefs.getString(_kStaffMail) ?? '';
_smtpHostCtrl.text = prefs.getString(_kSmtpHost) ?? '';
_smtpPortCtrl.text = prefs.getString(_kSmtpPort) ?? '587';
_smtpUserCtrl.text = prefs.getString(_kSmtpUser) ?? '';
_smtpPassCtrl.text = _decryptWithFallback(prefs.getString(_kSmtpPass) ?? '');
_smtpTls = prefs.getBool(_kSmtpTls) ?? true;
_smtpBccCtrl.text = prefs.getString(_kSmtpBcc) ?? '';
_externalHostCtrl.text = prefs.getString(_kExternalHost) ?? '';
_externalPassCtrl.text = prefs.getString(_kExternalPass) ?? '';
_backupPathCtrl.text = prefs.getString(_kBackupPath) ?? '';
});
}
@override
void initState() {
super.initState();
_loadAll();
}
void _showSnackbar(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
Future<void> _saveCompany() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kCompanyName, _companyNameCtrl.text);
await prefs.setString(_kCompanyZip, _companyZipCtrl.text);
await prefs.setString(_kCompanyAddr, _companyAddrCtrl.text);
await prefs.setString(_kCompanyTel, _companyTelCtrl.text);
await prefs.setString(_kCompanyReg, _companyRegCtrl.text);
await prefs.setString(_kCompanyFax, _companyFaxCtrl.text);
await prefs.setString(_kCompanyEmail, _companyEmailCtrl.text);
await prefs.setString(_kCompanyUrl, _companyUrlCtrl.text);
_showSnackbar('自社情報を保存しました');
}
Future<void> _saveStaff() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kStaffName, _staffNameCtrl.text);
await prefs.setString(_kStaffMail, _staffMailCtrl.text);
_showSnackbar('担当者情報を保存しました');
}
Future<void> _saveSmtp() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kSmtpHost, _smtpHostCtrl.text);
await prefs.setString(_kSmtpPort, _smtpPortCtrl.text);
await prefs.setString(_kSmtpUser, _smtpUserCtrl.text);
await prefs.setString(_kSmtpPass, _encrypt(_smtpPassCtrl.text));
await prefs.setBool(_kSmtpTls, _smtpTls);
await prefs.setString(_kSmtpBcc, _smtpBccCtrl.text);
_showSnackbar('SMTP設定を保存しました');
}
Future<void> _saveExternalSync() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kExternalHost, _externalHostCtrl.text);
await prefs.setString(_kExternalPass, _externalPassCtrl.text);
_showSnackbar('外部同期設定を保存しました');
}
Future<void> _saveBackup() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kBackupPath, _backupPathCtrl.text);
_showSnackbar('バックアップ設定を保存しました');
}
void _pickBackupPath() => _showSnackbar('バックアップ先の選択は後で実装');
Future<void> _loadKanaMap() async {
final prefs = await SharedPreferences.getInstance();
final json = prefs.getString('customKanaMap');
if (json != null && json.isNotEmpty) {
try {
final Map<String, dynamic> decoded = jsonDecode(json);
setState(() => _customKanaMap = decoded.map((k, v) => MapEntry(k, v.toString())));
} catch (_) {
// ignore
}
}
}
Future<void> _saveKanaMap() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('customKanaMap', jsonEncode(_customKanaMap));
_showSnackbar('かなインデックスを保存しました');
}
String _encrypt(String plain) {
if (plain.isEmpty) return '';
final pb = utf8.encode(plain);
final kb = utf8.encode(_kCryptKey);
final ob = List<int>.generate(pb.length, (i) => pb[i] ^ kb[i % kb.length]);
return base64Encode(ob);
}
String _decryptWithFallback(String cipher) {
if (cipher.isEmpty) return '';
try {
final ob = base64Decode(cipher);
final kb = utf8.encode(_kCryptKey);
final pb = List<int>.generate(ob.length, (i) => ob[i] ^ kb[i % kb.length]);
return utf8.decode(pb);
} catch (_) {
return cipher; // 旧プレーンテキストも許容
}
}
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
final listBottomPadding = 24 + bottomInset;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('S1:設定'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () => _showSnackbar('設定はテンプレ実装です。実際の保存は未実装'),
),
],
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: ListView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(bottom: listBottomPadding),
children: [
_section(
title: '自社情報',
subtitle: '会社名・住所・登録番号など',
child: Column(
children: [
TextField(controller: _companyNameCtrl, decoration: const InputDecoration(labelText: '会社名')),
TextField(controller: _companyZipCtrl, decoration: const InputDecoration(labelText: '郵便番号')),
TextField(controller: _companyAddrCtrl, decoration: const InputDecoration(labelText: '住所')),
TextField(controller: _companyTelCtrl, decoration: const InputDecoration(labelText: '電話番号')),
TextField(controller: _companyFaxCtrl, decoration: const InputDecoration(labelText: 'FAX番号')),
TextField(controller: _companyEmailCtrl, decoration: const InputDecoration(labelText: 'メールアドレス')),
TextField(controller: _companyUrlCtrl, decoration: const InputDecoration(labelText: 'URL')),
TextField(controller: _companyRegCtrl, decoration: const InputDecoration(labelText: '登録番号 (インボイス)')),
const SizedBox(height: 8),
Row(
children: [
OutlinedButton.icon(
icon: const Icon(Icons.upload_file),
label: const Text('画面で編集'),
onPressed: () async {
await Navigator.push(context, MaterialPageRoute(builder: (context) => const CompanyInfoScreen()));
},
),
const SizedBox(width: 8),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: _saveCompany,
),
],
),
],
),
),
_section(
title: '担当者情報',
subtitle: '署名や連絡先(送信者情報)',
child: Column(
children: [
TextField(controller: _staffNameCtrl, decoration: const InputDecoration(labelText: '担当者名')),
TextField(controller: _staffMailCtrl, decoration: const InputDecoration(labelText: 'メールアドレス')),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: _saveStaff,
),
],
),
),
_section(
title: 'SMTP情報',
subtitle: 'メール送信サーバ設定(テンプレ)',
child: Column(
children: [
TextField(controller: _smtpHostCtrl, decoration: const InputDecoration(labelText: 'ホスト名')),
TextField(controller: _smtpPortCtrl, decoration: const InputDecoration(labelText: 'ポート番号'), keyboardType: TextInputType.number),
TextField(controller: _smtpUserCtrl, decoration: const InputDecoration(labelText: 'ユーザー名')),
TextField(controller: _smtpPassCtrl, decoration: const InputDecoration(labelText: 'パスワード'), obscureText: true),
TextField(controller: _smtpBccCtrl, decoration: const InputDecoration(labelText: 'BCC (カンマ区切り可)')),
SwitchListTile(
title: const Text('STARTTLS を使用'),
value: _smtpTls,
onChanged: (v) => setState(() => _smtpTls = v),
),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: _saveSmtp,
),
],
),
),
_section(
title: '外部同期(母艦システム「お局様」連携)',
subtitle: '実行ボタンなし。ホストドメインとパスワードを入力してください。',
child: Column(
children: [
TextField(controller: _externalHostCtrl, decoration: const InputDecoration(labelText: 'ホストドメイン')),
TextField(controller: _externalPassCtrl, decoration: const InputDecoration(labelText: 'パスワード'), obscureText: true),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: _saveExternalSync,
),
],
),
),
_section(
title: 'バックアップドライブ',
subtitle: 'バックアップ先のクラウド/ローカル',
child: Column(
children: [
TextField(controller: _backupPathCtrl, decoration: const InputDecoration(labelText: '保存先パス/URL')),
const SizedBox(height: 8),
Row(
children: [
OutlinedButton.icon(
icon: const Icon(Icons.folder_open),
label: const Text('参照'),
onPressed: _pickBackupPath,
),
const SizedBox(width: 8),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: _saveBackup,
),
],
),
],
),
),
_section(
title: 'テーマ選択',
subtitle: '配色や見た目を切り替え(テンプレ)',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<String>(
initialValue: _theme,
decoration: const InputDecoration(labelText: 'テーマを選択'),
items: const [
DropdownMenuItem(value: 'light', child: Text('ライト')),
DropdownMenuItem(value: 'dark', child: Text('ダーク')),
DropdownMenuItem(value: 'system', child: Text('システムに従う')),
],
onChanged: (v) => setState(() => _theme = v ?? 'system'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: () => _showSnackbar('テーマ設定を保存(テンプレ): $_theme'),
),
],
),
),
_section(
title: 'かなインデックス追加',
subtitle: '漢字→行1文字ずつを追加して索引を補強',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _kanaKeyCtrl,
maxLength: 1,
decoration: const InputDecoration(labelText: '漢字1文字', counterText: ''),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _kanaValCtrl,
maxLength: 1,
decoration: const InputDecoration(labelText: '行(例: さ)', counterText: ''),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
final k = _kanaKeyCtrl.text.trim();
final v = _kanaValCtrl.text.trim();
if (k.isEmpty || v.isEmpty) return;
setState(() {
_customKanaMap[k] = v;
});
},
child: const Text('追加'),
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
children: _customKanaMap.entries
.map((e) => Chip(
label: Text('${e.key}: ${e.value}'),
onDeleted: () => setState(() => _customKanaMap.remove(e.key)),
))
.toList(),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.save),
label: const Text('保存'),
onPressed: _saveKanaMap,
),
],
),
),
],
),
),
);
}
Widget _section({required String title, required String subtitle, required Widget child}) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(subtitle, style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 12),
child,
],
),
),
);
}
}