diff --git "a/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md" "b/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md" index d63c009..4e1d4b9 100644 --- "a/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md" +++ "b/docs/04_SPEC/SPEC_01_\347\224\273\351\235\242\346\251\237\350\203\275\344\273\225\346\247\230\346\233\270.md" @@ -25,14 +25,14 @@ │ サムネイルタップ ▼ ┌──────────┐ - │ 拡大表示 │ - │ (ダイアログ) │ + │ 詳細表示 │ + │(インライン) │ └──────────┘ ``` - アプリ起動時のデフォルト画面は **撮影画面(S-01)** とする - 画面切り替えには `BottomNavigationBar` を使用する(タブ 2 つ: 撮影 / 一覧) -- 画像の拡大表示はダイアログ(`showDialog`)で実装し,独立画面としない +- 画像の詳細表示は一覧画面内の `Stack` オーバーレイで実装し,独立画面・ダイアログとしない ## 撮影画面 (S-01: Capture Screen) @@ -65,7 +65,7 @@ | シャッターボタン | 丸型ボタン(画面下部中央).タップで撮影実行.連続撮影可 | | タイマーボタン | 画面左下に配置.ON(黄色)/ OFF(グレー)を切り替える.ON 時はシャッター押下後 3 秒のカウントダウン表示の後に撮影 | | 明るさスライダー | ナビバーの直下に配置.画面の輝度を手動で調整する(デフォルト: 0.8) | -| BottomNavigationBar | 撮影タブ(アクティブ)/ 一覧タブ.背景色は黒系で統一 | +| BottomNavigationBar | 撮影タブ(アクティブ)/ 一覧タブ.背景色は黒系で統一.保存中はタップ無効 | ### 動作仕様 @@ -112,20 +112,21 @@ | ファイル名ラベル | 各サムネイル下部にファイル名を表示 | | BottomNavigationBar | 撮影タブ / 一覧タブ(アクティブ) | -### 拡大表示ダイアログ +### 詳細表示(インラインオーバーレイ) | 要素 | 仕様 | | --- | --- | -| 画像 | 元画像をフル解像度で表示.ピンチ操作でズーム可能 | -| ファイル名 | ダイアログ上部にファイル名を表示 | -| 削除ボタン | ダイアログ内に配置.タップで削除確認ダイアログを表示 | -| 閉じるボタン | ダイアログ外タップまたは閉じるボタンで閉じる | +| 画像 | 一覧画面エリアを全面に覆うオーバーレイで表示.カメラプレビューと同様に上下をクロップして全面表示(`BoxFit.cover`).ピンチ操作でズーム可能 | +| ファイル名 | オーバーレイ上部に半透明背景とともに表示 | +| 削除ボタン | オーバーレイ下部に半透明背景とともに配置.タップで削除確認ダイアログを表示 | +| 閉じるボタン | ヘッダー右端の × ボタンで閉じる | ### 動作仕様 - **画面表示時**: `Pictures/MiniTIAS/` ディレクトリ内の PNG ファイルを走査し,一覧を構築する - **リフレッシュ**: 撮影画面から戻った際に一覧を再取得する -- **削除**: 削除確認ダイアログで「削除」を選択すると,ファイルをストレージから削除し,一覧から除去する +- **詳細表示**: サムネイルタップで詳細表示オーバーレイを開く.× ボタンで閉じる +- **削除**: 詳細表示の削除ボタンから確認ダイアログで「削除」を選択すると,ファイルをストレージから削除し,一覧から除去する - **空状態**: 画像が 0 件の場合は「撮影した画像がありません」のメッセージを表示する ## カメラ制御仕様 (Camera Control) @@ -229,7 +230,7 @@ ### 削除 -- 一覧画面の拡大表示ダイアログから削除を実行する +- 一覧画面の詳細表示オーバーレイから削除を実行する - 削除前に確認ダイアログを表示する(「この画像を削除しますか?」) - ファイルシステムから物理削除する(ゴミ箱機能なし) - 削除後,MediaStore からも当該エントリを除去する @@ -297,8 +298,7 @@ ├── widgets/ │ ├── camera_preview.dart # カメラプレビューウィジェット │ ├── shutter_button.dart # シャッターボタン -│ ├── image_grid.dart # サムネイルグリッド -│ └── image_detail_dialog.dart # 拡大表示ダイアログ +│ └── image_grid.dart # サムネイルグリッド └── services/ ├── file_service.dart # ファイル保存・命名・削除 └── permission_service.dart # パーミッション管理 diff --git a/lib/screens/gallery_screen.dart b/lib/screens/gallery_screen.dart index 5e08a72..1f43965 100644 --- a/lib/screens/gallery_screen.dart +++ b/lib/screens/gallery_screen.dart @@ -1,8 +1,9 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mini_tias/providers/gallery_provider.dart'; -import 'package:mini_tias/widgets/image_detail_dialog.dart'; import 'package:mini_tias/widgets/image_grid.dart'; /// 一覧画面.撮影済み画像のサムネイルグリッドを表示する. @@ -14,6 +15,8 @@ } class _GalleryScreenState extends State { + File? _selectedFile; + @override void initState() { super.initState(); @@ -34,19 +37,114 @@ return const Center(child: Text('撮影した画像がありません')); } - return ImageGrid( - images: galleryProvider.images, - onImageTap: (file) { - showDialog( - context: context, - builder: (_) => ImageDetailDialog( - file: file, - onDelete: () { + return Stack( + children: [ + ImageGrid( + images: galleryProvider.images, + onImageTap: (file) => setState(() => _selectedFile = file), + ), + // 画像詳細表示(この領域内に収まる) + if (_selectedFile != null) + Positioned.fill( + child: Stack( + children: [ + // 画像(全面・上下クロップ) + InteractiveViewer( + minScale: 1.0, + maxScale: 5.0, + child: SizedBox.expand( + child: Image.file( + _selectedFile!, + fit: BoxFit.cover, + ), + ), + ), + // ヘッダー(ファイル名 + 閉じるボタン) + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + color: Colors.black54, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Row( + children: [ + Expanded( + child: Text( + _selectedFile!.path.split('/').last, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon( + Icons.close, + color: Colors.white, + ), + onPressed: () => + setState(() => _selectedFile = null), + ), + ], + ), + ), + ), + // 削除ボタン + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + color: Colors.black54, + padding: const EdgeInsets.all(8), + child: Center( + child: TextButton.icon( + onPressed: () => _confirmDelete(galleryProvider), + icon: const Icon(Icons.delete, color: Colors.red), + label: const Text( + '削除', + style: TextStyle(color: Colors.red), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ); + } + + void _confirmDelete(GalleryProvider galleryProvider) { + final file = _selectedFile; + if (file == null) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('画像を削除'), + content: const Text('この画像を削除しますか?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('キャンセル'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + setState(() => _selectedFile = null); galleryProvider.deleteImage(file); }, + child: const Text('削除', style: TextStyle(color: Colors.red)), ), - ); - }, + ], + ), ); } } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 47b1bd9..7cb181d 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -48,30 +48,31 @@ backgroundColor: Colors.black, body: Column( children: [ - // ナビゲーションバー - BottomNavigationBar( - currentIndex: _currentIndex, - onTap: cameraProvider.isSaving - ? null - : (index) { - setState(() => _currentIndex = index); - if (index == 1) { - context.read().loadImages(); - } - }, - backgroundColor: Colors.black87, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white60, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.camera_alt), - label: '撮影', - ), - BottomNavigationBarItem( - icon: Icon(Icons.photo_library), - label: '一覧', - ), - ], + // ナビゲーションバー(保存中はタップ無効) + AbsorbPointer( + absorbing: cameraProvider.isSaving, + child: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: (index) { + setState(() => _currentIndex = index); + if (index == 1) { + context.read().loadImages(); + } + }, + backgroundColor: Colors.black87, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.white60, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.camera_alt), + label: '撮影', + ), + BottomNavigationBarItem( + icon: Icon(Icons.photo_library), + label: '一覧', + ), + ], + ), ), // 明るさスライダー diff --git a/lib/widgets/image_detail_dialog.dart b/lib/widgets/image_detail_dialog.dart deleted file mode 100644 index c014875..0000000 --- a/lib/widgets/image_detail_dialog.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -/// 画像の拡大表示ダイアログ.ピンチズームと削除ボタンを備える. -class ImageDetailDialog extends StatelessWidget { - const ImageDetailDialog({ - super.key, - required this.file, - required this.onDelete, - }); - - final File file; - final VoidCallback onDelete; - - @override - Widget build(BuildContext context) { - final fileName = file.path.split('/').last; - - return Dialog( - backgroundColor: Colors.black, - insetPadding: const EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // ヘッダー(ファイル名 + 閉じるボタン) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Row( - children: [ - Expanded( - child: Text( - fileName, - style: const TextStyle(color: Colors.white, fontSize: 12), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ), - // 画像(ピンチズーム対応) - Flexible( - child: InteractiveViewer( - minScale: 1.0, - maxScale: 5.0, - child: Image.file(file), - ), - ), - // 削除ボタン - Padding( - padding: const EdgeInsets.all(8), - child: TextButton.icon( - onPressed: () => _confirmDelete(context), - icon: const Icon(Icons.delete, color: Colors.red), - label: const Text('削除', style: TextStyle(color: Colors.red)), - ), - ), - ], - ), - ); - } - - void _confirmDelete(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('画像を削除'), - content: const Text('この画像を削除しますか?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('キャンセル'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); // 確認ダイアログを閉じる - Navigator.of(context).pop(); // 詳細ダイアログを閉じる - onDelete(); - }, - child: const Text('削除', style: TextStyle(color: Colors.red)), - ), - ], - ), - ); - } -}