feat(ui): Implement Developer Menu & A/B Testing (Grid/FAB)
This commit is contained in:
parent
d0ce82f59a
commit
fbeefd9456
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
// UI実験設定クラス
|
||||||
|
class UiExperimentSettings {
|
||||||
|
final int gridColumns; // 2 or 3
|
||||||
|
final String fabAnimation; // 'rotate' or 'bounce'
|
||||||
|
|
||||||
|
const UiExperimentSettings({
|
||||||
|
this.gridColumns = 2,
|
||||||
|
this.fabAnimation = 'rotate',
|
||||||
|
});
|
||||||
|
|
||||||
|
UiExperimentSettings copyWith({
|
||||||
|
int? gridColumns,
|
||||||
|
String? fabAnimation,
|
||||||
|
}) {
|
||||||
|
return UiExperimentSettings(
|
||||||
|
gridColumns: gridColumns ?? this.gridColumns,
|
||||||
|
fabAnimation: fabAnimation ?? this.fabAnimation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider
|
||||||
|
final uiExperimentProvider = StateNotifierProvider<UiExperimentNotifier, UiExperimentSettings>(
|
||||||
|
(ref) => UiExperimentNotifier(),
|
||||||
|
);
|
||||||
|
|
||||||
|
class UiExperimentNotifier extends StateNotifier<UiExperimentSettings> {
|
||||||
|
UiExperimentNotifier() : super(const UiExperimentSettings());
|
||||||
|
|
||||||
|
void setGridColumns(int columns) {
|
||||||
|
state = state.copyWith(gridColumns: columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFabAnimation(String animation) {
|
||||||
|
state = state.copyWith(fabAnimation: animation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import '../providers/ui_experiment_provider.dart';
|
||||||
|
|
||||||
|
class DevMenuScreen extends ConsumerWidget {
|
||||||
|
const DevMenuScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final experiment = ref.watch(uiExperimentProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('🔬 開発者メニュー'),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
const ListTile(
|
||||||
|
leading: Icon(LucideIcons.flaskConical),
|
||||||
|
title: Text('UI実験'),
|
||||||
|
subtitle: Text('新しいデザインのテスト'),
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Text('📱', style: TextStyle(fontSize: 24)),
|
||||||
|
title: const Text('グリッド3列表示'),
|
||||||
|
subtitle: Text('瓶型の縦長カード (現在: ${experiment.gridColumns}列)'),
|
||||||
|
value: experiment.gridColumns == 3,
|
||||||
|
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
|
||||||
|
.setGridColumns(val ? 3 : 2),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Text('✨', style: TextStyle(fontSize: 24)),
|
||||||
|
title: const Text('FABバウンス'),
|
||||||
|
subtitle: Text('ぷるんとした動き (現在: ${experiment.fabAnimation == 'bounce' ? 'ON' : 'OFF'})'),
|
||||||
|
value: experiment.fabAnimation == 'bounce',
|
||||||
|
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
|
||||||
|
.setFabAnimation(val ? 'bounce' : 'rotate'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,9 +24,10 @@ import '../widgets/onboarding_dialog.dart';
|
||||||
import '../widgets/home/sake_filter_chips.dart';
|
import '../widgets/home/sake_filter_chips.dart';
|
||||||
import '../widgets/home/home_empty_state.dart';
|
import '../widgets/home/home_empty_state.dart';
|
||||||
import '../widgets/home/sake_no_match_state.dart';
|
import '../widgets/home/sake_no_match_state.dart';
|
||||||
import '../widgets/home/sake_list_view.dart';
|
import '../models/user_profile.dart'; // UserProfile
|
||||||
import '../widgets/home/sake_grid_view.dart';
|
import '../widgets/analyzing_dialog.dart';
|
||||||
import '../widgets/add_set_item_dialog.dart';
|
import '../widgets/empty_state.dart'; // Generic empty state
|
||||||
|
import '../providers/ui_experiment_provider.dart'; // A/B Test
|
||||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import '../widgets/prefecture_filter_sheet.dart';
|
import '../widgets/prefecture_filter_sheet.dart';
|
||||||
|
|
@ -210,6 +211,15 @@ class HomeScreen extends ConsumerWidget {
|
||||||
activeBackgroundColor: Colors.grey[800],
|
activeBackgroundColor: Colors.grey[800],
|
||||||
overlayColor: Colors.black,
|
overlayColor: Colors.black,
|
||||||
overlayOpacity: 0.5,
|
overlayOpacity: 0.5,
|
||||||
|
|
||||||
|
// A/B Test Animation
|
||||||
|
animationCurve: ref.watch(uiExperimentProvider).fabAnimation == 'bounce'
|
||||||
|
? Curves.elasticOut
|
||||||
|
: Curves.linear,
|
||||||
|
animationDuration: ref.watch(uiExperimentProvider).fabAnimation == 'bounce'
|
||||||
|
? const Duration(milliseconds: 400)
|
||||||
|
: const Duration(milliseconds: 250),
|
||||||
|
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
spaceBetweenChildren: 12,
|
spaceBetweenChildren: 12,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; // HapticFeedback
|
||||||
|
|
||||||
import '../../models/sake_item.dart';
|
import '../../models/sake_item.dart';
|
||||||
import '../../providers/sake_list_provider.dart'; // For sakeOrderControllerProvider
|
import '../../providers/sake_list_provider.dart'; // For sakeOrderControllerProvider
|
||||||
|
import '../../providers/ui_experiment_provider.dart'; // A/B Test
|
||||||
import 'sake_grid_item.dart';
|
import 'sake_grid_item.dart';
|
||||||
|
|
||||||
class SakeGridView extends ConsumerWidget {
|
class SakeGridView extends ConsumerWidget {
|
||||||
|
|
@ -23,15 +24,17 @@ class SakeGridView extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final list = sakeList.cast<SakeItem>();
|
final list = sakeList.cast<SakeItem>();
|
||||||
|
|
||||||
|
final experiment = ref.watch(uiExperimentProvider);
|
||||||
|
|
||||||
// If reorder is disabled (Menu Creation Screen), use standard GridView
|
// If reorder is disabled (Menu Creation Screen), use standard GridView
|
||||||
if (!enableReorder || isMenuMode) {
|
if (!enableReorder || isMenuMode) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: experiment.gridColumns,
|
||||||
crossAxisSpacing: 4,
|
crossAxisSpacing: 4,
|
||||||
mainAxisSpacing: 4,
|
mainAxisSpacing: 4,
|
||||||
childAspectRatio: 1.0,
|
childAspectRatio: experiment.gridColumns == 3 ? (1.0 / 1.5) : 1.0,
|
||||||
),
|
),
|
||||||
itemCount: list.length,
|
itemCount: list.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|
@ -47,11 +50,11 @@ class SakeGridView extends ConsumerWidget {
|
||||||
// Standard ReorderableGridView for Home Screen
|
// Standard ReorderableGridView for Home Screen
|
||||||
return ReorderableGridView.builder(
|
return ReorderableGridView.builder(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: experiment.gridColumns,
|
||||||
crossAxisSpacing: 4,
|
crossAxisSpacing: 4,
|
||||||
mainAxisSpacing: 4,
|
mainAxisSpacing: 4,
|
||||||
childAspectRatio: 1.0,
|
childAspectRatio: experiment.gridColumns == 3 ? (1.0 / 1.5) : 1.0,
|
||||||
),
|
),
|
||||||
itemCount: list.length,
|
itemCount: list.length,
|
||||||
onReorder: (oldIndex, newIndex) {
|
onReorder: (oldIndex, newIndex) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import '../../screens/dev_menu_screen.dart';
|
||||||
import '../../providers/theme_provider.dart';
|
import '../../providers/theme_provider.dart';
|
||||||
|
|
||||||
class OtherSettingsSection extends ConsumerStatefulWidget {
|
class OtherSettingsSection extends ConsumerStatefulWidget {
|
||||||
|
|
@ -20,6 +22,7 @@ class OtherSettingsSection extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
|
class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
|
||||||
String _appVersion = 'Loading...';
|
String _appVersion = 'Loading...';
|
||||||
|
int _devTapCount = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -60,11 +63,27 @@ class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(LucideIcons.info, color: isDark ? Colors.grey[400] : null),
|
leading: Icon(LucideIcons.info, color: isDark ? Colors.grey[400] : null),
|
||||||
title: const Text('アプリバージョン'),
|
title: const Text('アプリバージョン'),
|
||||||
subtitle: Text(_appVersion),
|
subtitle: Text(_appVersion),
|
||||||
),
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_devTapCount++;
|
||||||
|
if (_devTapCount >= 5) {
|
||||||
|
_devTapCount = 0;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const DevMenuScreen()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Optional: Small feedback
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue