分割に失敗、修復中

This commit is contained in:
joe 2026-02-26 17:29:22 +09:00
parent 504a5a60cc
commit c01a0b6775
11 changed files with 966 additions and 764 deletions

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../models/company_model.dart'; import '../models/company_model.dart';
import '../services/company_repository.dart'; import '../services/company_repository.dart';
import '../widgets/keyboard_inset_wrapper.dart';
class CompanyInfoScreen extends StatefulWidget { class CompanyInfoScreen extends StatefulWidget {
const CompanyInfoScreen({Key? key}) : super(key: key); const CompanyInfoScreen({Key? key}) : super(key: key);
@ -76,8 +77,11 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
IconButton(icon: const Icon(Icons.check), onPressed: _save), IconButton(icon: const Icon(Icons.check), onPressed: _save),
], ],
), ),
body: SingleChildScrollView( body: KeyboardInsetWrapper(
padding: const EdgeInsets.all(16), basePadding: const EdgeInsets.all(16),
extraBottom: 32,
child: SingleChildScrollView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -142,6 +146,7 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
], ],
), ),
), ),
),
); );
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import '../widgets/keyboard_inset_wrapper.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert'; import 'dart:convert';
import '../models/customer_model.dart'; import '../models/customer_model.dart';
@ -315,9 +316,14 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final result = await showDialog<Customer>( final result = await showDialog<Customer>(
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog( builder: (context, setDialogState) {
return AlertDialog(
title: Text(isEdit ? "顧客を編集" : "顧客を新規登録"), title: Text(isEdit ? "顧客を編集" : "顧客を新規登録"),
content: SingleChildScrollView( content: KeyboardInsetWrapper(
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
extraBottom: 20,
child: SingleChildScrollView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -423,6 +429,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
], ],
), ),
), ),
),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")), TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton( TextButton(
@ -449,7 +456,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
child: const Text("保存"), child: const Text("保存"),
), ),
], ],
), );
},
), ),
); );
@ -741,7 +749,10 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
], ],
), ),
body: Column( body: KeyboardInsetWrapper(
basePadding: const EdgeInsets.fromLTRB(0, 8, 0, 80),
extraBottom: 40,
child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@ -757,7 +768,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
onChanged: (_) => setState(_applyFilter), onChanged: (_) => setState(_applyFilter),
), ),
), ),
// Kana index temporarily disabled
if (!widget.selectionMode) if (!widget.selectionMode)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
@ -776,6 +786,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
: _filtered.isEmpty : _filtered.isEmpty
? const Center(child: Text("顧客が登録されていません")) ? const Center(child: Text("顧客が登録されていません"))
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.only(bottom: 120, top: 4),
itemCount: _filtered.length, itemCount: _filtered.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final c = _filtered[index]; final c = _filtered[index];
@ -807,10 +818,11 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
], ],
), ),
),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: _showAddMenu, onPressed: _showAddMenu,
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('顧客を追加'), label: Text(widget.selectionMode ? "選択" : "追加"),
backgroundColor: Colors.indigo, backgroundColor: Colors.indigo,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
@ -963,7 +975,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
_showContactUpdateDialog(c); _showContactUpdateSheet(c);
}, },
icon: const Icon(Icons.contact_mail), icon: const Icon(Icons.contact_mail),
label: const Text("連絡先を更新"), label: const Text("連絡先を更新"),
@ -1006,4 +1018,38 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
); );
} }
void _showContactUpdateSheet(Customer c) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => KeyboardInsetWrapper(
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
extraBottom: 16,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.contact_mail),
title: const Text('連絡先を更新'),
onTap: () {
Navigator.pop(context);
_showContactUpdateSheet(c);
},
),
ListTile(
leading: const Icon(Icons.contact_phone),
title: const Text('電話帳から取り込む'),
onTap: () {
Navigator.pop(context);
_showPhonebookImport();
},
),
],
),
),
),
);
}
} }

View file

@ -3,6 +3,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../models/customer_model.dart'; import '../models/customer_model.dart';
import '../services/customer_repository.dart'; import '../services/customer_repository.dart';
import '../widgets/keyboard_inset_wrapper.dart';
/// ///
class CustomerPickerModal extends StatefulWidget { class CustomerPickerModal extends StatefulWidget {
@ -203,6 +204,9 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
child: KeyboardInsetWrapper(
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
extraBottom: 24,
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -241,13 +245,15 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
], ],
), ),
), ),
const Divider(), const Divider(height: 1),
Expanded( Expanded(
child: _isLoading child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _filteredCustomers.isEmpty : _filteredCustomers.isEmpty
? const Center(child: Text("該当する顧客がいません")) ? const Center(child: Text("該当する顧客がいません"))
: ListView.builder( : ListView.builder(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: const EdgeInsets.only(bottom: 80),
itemCount: _filteredCustomers.length, itemCount: _filteredCustomers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final customer = _filteredCustomers[index]; final customer = _filteredCustomers[index];
@ -279,6 +285,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
), ),
], ],
), ),
),
); );
} }
} }

View file

@ -13,6 +13,7 @@ import '../services/customer_repository.dart';
import '../services/company_repository.dart'; import '../services/company_repository.dart';
import 'product_picker_modal.dart'; import 'product_picker_modal.dart';
import '../models/company_model.dart'; import '../models/company_model.dart';
import '../widgets/keyboard_inset_wrapper.dart';
class InvoiceDetailPage extends StatefulWidget { class InvoiceDetailPage extends StatefulWidget {
final Invoice invoice; final Invoice invoice;
@ -158,6 +159,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
return Scaffold( return Scaffold(
backgroundColor: themeColor, backgroundColor: themeColor,
resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
leading: const BackButton(), // leading: const BackButton(), //
title: Row( title: Row(
@ -245,8 +247,11 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
] ]
], ],
), ),
body: SingleChildScrollView( body: KeyboardInsetWrapper(
padding: const EdgeInsets.all(16.0), basePadding: const EdgeInsets.all(16.0),
extraBottom: 48,
child: SingleChildScrollView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -315,11 +320,11 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
], ],
), ),
), ),
),
); );
} }
Widget _buildHeaderSection(Color textColor) { Widget _buildHeaderSection(Color textColor) {
final dateFormatter = DateFormat('yyyy年MM月dd日');
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View file

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../models/invoice_models.dart';
class InvoiceHistoryItem extends StatelessWidget {
final Invoice invoice;
final bool isUnlocked;
final NumberFormat amountFormatter;
final DateFormat dateFormatter;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final VoidCallback? onEdit;
const InvoiceHistoryItem({
Key? key,
required this.invoice,
required this.isUnlocked,
required this.amountFormatter,
required this.dateFormatter,
this.onTap,
this.onLongPress,
this.onEdit,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
tileColor: invoice.isDraft ? Colors.orange.shade50 : null,
leading: CircleAvatar(
backgroundColor: invoice.isDraft
? Colors.orange.shade100
: (isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Icon(
invoice.isDraft ? Icons.edit_note : Icons.description_outlined,
color: invoice.isDraft
? Colors.orange
: (isUnlocked ? Colors.indigo : Colors.grey),
),
),
if (invoice.isLocked)
const Align(
alignment: Alignment.bottomRight,
child: Icon(Icons.lock, size: 14, color: Colors.redAccent),
),
],
),
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
invoice.customerNameForDisplay,
style: TextStyle(
fontWeight: FontWeight.bold,
color: invoice.isLocked ? Colors.grey : Colors.black87,
),
),
if (invoice.subject?.isNotEmpty ?? false)
Text(
invoice.subject!,
style: TextStyle(
fontSize: 13,
color: Colors.indigo.shade700,
fontWeight: FontWeight.normal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
trailing: SizedBox(
height: 48,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${amountFormatter.format(invoice.totalAmount)}",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
if (invoice.isSynced)
const Icon(Icons.sync, size: 14, color: Colors.green)
else
const Icon(Icons.sync_disabled, size: 14, color: Colors.orange),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(width: 28, height: 24),
icon: const Icon(Icons.edit, size: 16),
tooltip: invoice.isLocked
? "ロック中"
: (isUnlocked ? "編集" : "アンロックして編集"),
onPressed: (invoice.isLocked || !isUnlocked)
? null
: onEdit,
),
],
),
),
onTap: onTap,
onLongPress: onLongPress,
);
}
}

View file

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../models/invoice_models.dart';
import 'invoice_history_item.dart';
class InvoiceHistoryList extends StatelessWidget {
final List<Invoice> invoices;
final bool isUnlocked;
final NumberFormat amountFormatter;
final DateFormat dateFormatter;
final void Function(Invoice) onTap;
final void Function(Invoice) onLongPress;
final void Function(Invoice) onEdit;
const InvoiceHistoryList({
Key? key,
required this.invoices,
required this.isUnlocked,
required this.amountFormatter,
required this.dateFormatter,
required this.onTap,
required this.onLongPress,
required this.onEdit,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (invoices.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.folder_open, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text("保存された伝票がありません"),
],
),
);
}
return ListView.builder(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: const EdgeInsets.only(bottom: 120), // FAB分の固定余白
itemCount: invoices.length,
itemBuilder: (context, index) {
final invoice = invoices[index];
return InvoiceHistoryItem(
invoice: invoice,
isUnlocked: isUnlocked,
amountFormatter: amountFormatter,
dateFormatter: dateFormatter,
onTap: () => onTap(invoice),
onLongPress: () => onLongPress(invoice),
onEdit: () => onEdit(invoice),
);
},
);
}
}

View file

@ -17,6 +17,7 @@ import '../main.dart'; // InvoiceFlowScreen 用
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import '../widgets/invoice_pdf_preview_page.dart'; import '../widgets/invoice_pdf_preview_page.dart';
import 'invoice_history/invoice_history_list.dart';
class InvoiceHistoryScreen extends StatefulWidget { class InvoiceHistoryScreen extends StatefulWidget {
const InvoiceHistoryScreen({Key? key}) : super(key: key); const InvoiceHistoryScreen({Key? key}) : super(key: key);
@ -341,79 +342,26 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
Expanded( Expanded(
child: _isLoading child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _filteredInvoices.isEmpty : InvoiceHistoryList(
? Center( invoices: _filteredInvoices,
child: Column( isUnlocked: _isUnlocked,
mainAxisAlignment: MainAxisAlignment.center, amountFormatter: amountFormatter,
children: [ dateFormatter: dateFormatter,
const Icon(Icons.folder_open, size: 64, color: Colors.grey), onTap: (invoice) async {
const SizedBox(height: 16), await Navigator.push(
Text(_searchQuery.isEmpty ? "保存された伝票がありません" : "該当する伝票が見つかりません"), context,
], MaterialPageRoute(
), builder: (context) => InvoiceDetailPage(
) invoice: invoice,
: ListView.builder( isUnlocked: _isUnlocked,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: const EdgeInsets.only(bottom: 120), // : FAB+
itemCount: _filteredInvoices.length,
itemBuilder: (context, index) {
final invoice = _filteredInvoices[index];
return ListTile(
tileColor: invoice.isDraft ? Colors.orange.shade50 : null, //
leading: CircleAvatar(
backgroundColor: invoice.isDraft
? Colors.orange.shade100
: (_isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Icon(
invoice.isDraft ? Icons.edit_note : Icons.description_outlined,
color: invoice.isDraft
? Colors.orange
: (_isUnlocked ? Colors.indigo : Colors.grey),
), ),
), ),
if (invoice.isLocked) );
const Align(alignment: Alignment.bottomRight, child: Icon(Icons.lock, size: 14, color: Colors.redAccent)), _loadData();
], },
), onLongPress: (invoice) => _isUnlocked ? _showInvoiceActions(invoice) : _requireUnlock(),
), onEdit: (invoice) async {
title: Column( if (invoice.isLocked || !_isUnlocked) return;
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(invoice.customerNameForDisplay, style: TextStyle(fontWeight: FontWeight.bold, color: invoice.isLocked ? Colors.grey : Colors.black87)),
if (invoice.subject?.isNotEmpty ?? false)
Text(
invoice.subject!,
style: TextStyle(fontSize: 13, color: Colors.indigo.shade700, fontWeight: FontWeight.normal),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
trailing: SizedBox(
height: 60,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("${amountFormatter.format(invoice.totalAmount)}",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
if (invoice.isSynced)
const Icon(Icons.sync, size: 14, color: Colors.green)
else
const Icon(Icons.sync_disabled, size: 14, color: Colors.orange),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(width: 32, height: 26),
icon: const Icon(Icons.edit, size: 18),
tooltip: invoice.isLocked ? "ロック中" : (_isUnlocked ? "編集" : "アンロックして編集"),
onPressed: (invoice.isLocked || !_isUnlocked)
? null
: () async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -426,27 +374,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
_loadData(); _loadData();
}, },
), ),
],
),
),
onTap: _isUnlocked
? () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(
invoice: invoice,
isUnlocked: _isUnlocked, //
),
),
);
_loadData();
}
: () => _requireUnlock(),
onLongPress: _isUnlocked ? () => _showInvoiceActions(invoice) : () => _requireUnlock(),
);
},
),
), ),
], ],
), ),

View file

@ -14,6 +14,7 @@ import 'customer_master_screen.dart';
import 'product_picker_modal.dart'; import 'product_picker_modal.dart';
import '../models/company_model.dart'; import '../models/company_model.dart';
import '../services/company_repository.dart'; import '../services/company_repository.dart';
import '../widgets/keyboard_inset_wrapper.dart';
class InvoiceInputForm extends StatefulWidget { class InvoiceInputForm extends StatefulWidget {
final Function(Invoice invoice, String filePath) onInvoiceGenerated; final Function(Invoice invoice, String filePath) onInvoiceGenerated;
@ -226,7 +227,9 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
), ),
body: Stack( body: Stack(
children: [ children: [
SafeArea( KeyboardInsetWrapper(
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
extraBottom: 24,
child: InteractiveViewer( child: InteractiveViewer(
panEnabled: false, panEnabled: false,
minScale: 0.8, minScale: 0.8,
@ -236,7 +239,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
children: [ children: [
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 140), padding: const EdgeInsets.fromLTRB(16, 16, 16, 160),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View file

@ -3,6 +3,7 @@ import 'package:uuid/uuid.dart';
import '../models/product_model.dart'; import '../models/product_model.dart';
import '../services/product_repository.dart'; import '../services/product_repository.dart';
import 'barcode_scanner_screen.dart'; import 'barcode_scanner_screen.dart';
import '../widgets/keyboard_inset_wrapper.dart';
class ProductMasterScreen extends StatefulWidget { class ProductMasterScreen extends StatefulWidget {
const ProductMasterScreen({Key? key}) : super(key: key); const ProductMasterScreen({Key? key}) : super(key: key);
@ -59,7 +60,10 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
builder: (context) => StatefulBuilder( builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog( builder: (context, setDialogState) => AlertDialog(
title: Text(product == null ? "商品追加" : "商品編集"), title: Text(product == null ? "商品追加" : "商品編集"),
content: SingleChildScrollView( content: KeyboardInsetWrapper(
basePadding: EdgeInsets.zero,
extraBottom: 16,
child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -90,6 +94,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
], ],
), ),
), ),
),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")), TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
ElevatedButton( ElevatedButton(
@ -150,11 +155,15 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
), ),
), ),
), ),
body: _isLoading body: KeyboardInsetWrapper(
basePadding: EdgeInsets.zero,
extraBottom: 72,
child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _filteredProducts.isEmpty : _filteredProducts.isEmpty
? const Center(child: Text("商品が見つかりません")) ? const Center(child: Text("商品が見つかりません"))
: ListView.builder( : ListView.builder(
padding: const EdgeInsets.only(bottom: 120, top: 8),
itemCount: _filteredProducts.length, itemCount: _filteredProducts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final p = _filteredProducts[index]; final p = _filteredProducts[index];
@ -180,6 +189,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
); );
}, },
), ),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _showEditDialog(), onPressed: () => _showEditDialog(),
child: const Icon(Icons.add), child: const Icon(Icons.add),

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../widgets/keyboard_inset_wrapper.dart';
import 'company_info_screen.dart'; import 'company_info_screen.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
@ -226,7 +227,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
@ -235,16 +235,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
IconButton( IconButton(
icon: const Icon(Icons.info_outline), icon: const Icon(Icons.info_outline),
onPressed: () => _showSnackbar('設定はテンプレ実装です。実際の保存は未実装'), onPressed: () => _showSnackbar('設定はテンプレ実装です。実際の保存は未実装'),
) ),
], ],
), ),
body: SafeArea( body: KeyboardInsetWrapper(
child: AnimatedPadding( basePadding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
duration: const Duration(milliseconds: 180), extraBottom: 40,
curve: Curves.easeOut,
padding: EdgeInsets.only(bottom: bottomInset),
child: ListView( child: ListView(
padding: const EdgeInsets.all(16),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: [ children: [
_section( _section(
@ -452,7 +449,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
], ],
), ),
), ),
),
); );
} }

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
/// Wraps content with SafeArea and animated bottom padding based on keyboard.
/// Use this to keep forms scrollable without Scaffold resizing.
class KeyboardInsetWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets basePadding;
final double extraBottom;
final Duration duration;
final Curve curve;
const KeyboardInsetWrapper({
Key? key,
required this.child,
this.basePadding = EdgeInsets.zero,
this.extraBottom = 0,
this.duration = const Duration(milliseconds: 180),
this.curve = Curves.easeOut,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return SafeArea(
child: AnimatedPadding(
duration: duration,
curve: curve,
padding: basePadding.add(EdgeInsets.only(bottom: bottomInset + extraBottom)),
child: child,
),
);
}
}