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/home_empty_state.dart';
|
||||
import '../widgets/home/sake_no_match_state.dart';
|
||||
import '../widgets/home/sake_list_view.dart';
|
||||
import '../widgets/home/sake_grid_view.dart';
|
||||
import '../widgets/add_set_item_dialog.dart';
|
||||
import '../models/user_profile.dart'; // UserProfile
|
||||
import '../widgets/analyzing_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:lucide_icons/lucide_icons.dart';
|
||||
import '../widgets/prefecture_filter_sheet.dart';
|
||||
|
|
@ -210,6 +211,15 @@ class HomeScreen extends ConsumerWidget {
|
|||
activeBackgroundColor: Colors.grey[800],
|
||||
overlayColor: Colors.black,
|
||||
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,
|
||||
spaceBetweenChildren: 12,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; // HapticFeedback
|
|||
|
||||
import '../../models/sake_item.dart';
|
||||
import '../../providers/sake_list_provider.dart'; // For sakeOrderControllerProvider
|
||||
import '../../providers/ui_experiment_provider.dart'; // A/B Test
|
||||
import 'sake_grid_item.dart';
|
||||
|
||||
class SakeGridView extends ConsumerWidget {
|
||||
|
|
@ -23,15 +24,17 @@ class SakeGridView extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final list = sakeList.cast<SakeItem>();
|
||||
|
||||
final experiment = ref.watch(uiExperimentProvider);
|
||||
|
||||
// If reorder is disabled (Menu Creation Screen), use standard GridView
|
||||
if (!enableReorder || isMenuMode) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(4),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: experiment.gridColumns,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
childAspectRatio: 1.0,
|
||||
childAspectRatio: experiment.gridColumns == 3 ? (1.0 / 1.5) : 1.0,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
|
@ -47,11 +50,11 @@ class SakeGridView extends ConsumerWidget {
|
|||
// Standard ReorderableGridView for Home Screen
|
||||
return ReorderableGridView.builder(
|
||||
padding: const EdgeInsets.all(4),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: experiment.gridColumns,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
childAspectRatio: 1.0,
|
||||
childAspectRatio: experiment.gridColumns == 3 ? (1.0 / 1.5) : 1.0,
|
||||
),
|
||||
itemCount: list.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.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';
|
||||
|
||||
class OtherSettingsSection extends ConsumerStatefulWidget {
|
||||
|
|
@ -20,6 +22,7 @@ class OtherSettingsSection extends ConsumerStatefulWidget {
|
|||
|
||||
class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
|
||||
String _appVersion = 'Loading...';
|
||||
int _devTapCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -64,6 +67,22 @@ class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
|
|||
leading: Icon(LucideIcons.info, color: isDark ? Colors.grey[400] : null),
|
||||
title: const Text('アプリバージョン'),
|
||||
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