rewrite drawer fixes #316

rewrites the drawer from scratch

add drawer shaddow
This commit is contained in:
Leptopoda 2021-10-25 19:14:39 +02:00 committed by Calcitem
parent b19c6354e3
commit 4525953b33
18 changed files with 827 additions and 736 deletions

View File

@ -28,7 +28,7 @@ import 'package:hive_flutter/hive_flutter.dart' show Box;
import 'package:path_provider/path_provider.dart';
import 'package:sanmill/generated/intl/l10n.dart';
import 'package:sanmill/models/display.dart';
import 'package:sanmill/screens/navigation_home_screen.dart';
import 'package:sanmill/screens/home.dart';
import 'package:sanmill/services/audios.dart';
import 'package:sanmill/services/enviornment_config.dart';
import 'package:sanmill/services/language_info.dart';
@ -103,7 +103,7 @@ class SanmillApp extends StatelessWidget {
snackBar: SnackBar(
content: Text(S.of(context).tapBackAgainToLeave),
),
child: const NavigationHomeScreen(),
child: const Home(),
),
);
},

View File

@ -32,7 +32,9 @@ import 'package:sanmill/shared/theme/app_theme.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPage extends StatelessWidget {
final String tag = "[about] ";
const AboutPage({Key? key}) : super(key: key);
static const String tag = "[about] ";
String get mode {
if (kDebugMode) {

View File

@ -240,8 +240,7 @@ class Board extends StatelessWidget {
1
];
final bool ltr = Directionality.of(context) == TextDirection.ltr;
final bool ltr = Directionality.of(context) == TextDirection.ltr;
if (ltr) {
for (final file in ['a', 'b', 'c', 'd', 'e', 'f', 'g']) {

View File

@ -53,6 +53,7 @@ double boardWidth = 0.0;
class GamePage extends StatefulWidget {
final EngineType engineType;
// TODO: use gameInstamce.enginetype
const GamePage(this.engineType, {Key? key}) : super(key: key);
@override
@ -886,12 +887,10 @@ class _GamePageState extends State<GamePage>
);
}
void onOptionButtonPressed() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => GameSettingsPage()),
);
}
void onOptionButtonPressed() => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const GameSettingsPage()),
);
void onMoveButtonPressed() {
final List<Widget> _historyNavigation = [

View File

@ -38,6 +38,7 @@ part 'package:sanmill/screens/game_settings/skill_level_slider.dart';
part 'package:sanmill/screens/game_settings/move_time_slider.dart';
class GameSettingsPage extends StatelessWidget {
const GameSettingsPage({Key? key}) : super(key: key);
static const List<String> _algorithmNames = ['Alpha-Beta', 'PVS', 'MTD(f)'];
static const String _tag = "[game_settings_page]";

View File

@ -0,0 +1,235 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'dart:typed_data';
import 'package:feedback/feedback.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sanmill/generated/intl/l10n.dart';
import 'package:sanmill/mill/game.dart';
import 'package:sanmill/screens/about_page.dart';
import 'package:sanmill/screens/game_page/game_page.dart';
import 'package:sanmill/screens/game_settings/game_settings_page.dart';
import 'package:sanmill/screens/personalization_settings/help_screen.dart';
import 'package:sanmill/screens/personalization_settings/personalization_settings_page.dart';
import 'package:sanmill/screens/rule_settings/rule_settings_page.dart';
import 'package:sanmill/services/engine/engine.dart';
import 'package:sanmill/services/storage/storage.dart';
import 'package:sanmill/shared/constants.dart';
import 'package:sanmill/shared/custom_drawer/custom_drawer.dart';
enum _DrawerIndex {
humanVsAi,
humanVsHuman,
aiVsAi,
preferences,
ruleSettings,
personalization,
feedback,
Help,
About,
}
/// Home View
///
/// this widget implements the home view of our app.
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with TickerProviderStateMixin {
final _controller = CustomDrawerController();
Widget _screenView = const GamePage(EngineType.humanVsAi);
_DrawerIndex _drawerIndex = _DrawerIndex.humanVsAi;
static const Map<_DrawerIndex, Widget> _gamePages = {
_DrawerIndex.humanVsAi: GamePage(EngineType.humanVsAi),
_DrawerIndex.humanVsHuman: GamePage(EngineType.humanVsHuman),
_DrawerIndex.aiVsAi: GamePage(EngineType.aiVsAi),
};
/// callback from drawer for replace screen
/// as user need with passing DrawerIndex (Enum index)
void _changeIndex(_DrawerIndex index) {
_controller.hideDrawer();
if (_drawerIndex == index && _drawerIndex != _DrawerIndex.feedback) {
return;
}
setState(() {
_drawerIndex = index;
switch (_drawerIndex) {
case _DrawerIndex.humanVsAi:
gameInstance.setWhoIsAi(EngineType.humanVsAi);
_screenView = _gamePages[_DrawerIndex.humanVsAi]!;
break;
case _DrawerIndex.humanVsHuman:
gameInstance.setWhoIsAi(EngineType.humanVsHuman);
_screenView = _gamePages[_DrawerIndex.humanVsHuman]!;
break;
case _DrawerIndex.aiVsAi:
gameInstance.setWhoIsAi(EngineType.aiVsAi);
_screenView = _gamePages[_DrawerIndex.aiVsAi]!;
break;
case _DrawerIndex.preferences:
_screenView = const GameSettingsPage();
break;
case _DrawerIndex.ruleSettings:
_screenView = const RuleSettingsPage();
break;
case _DrawerIndex.personalization:
_screenView = const PersonalizationSettingsPage();
break;
case _DrawerIndex.feedback:
if (!LocalDatabaseService.preferences.developerMode) {
if (Platform.isWindows) {
debugPrint("flutter_email_sender does not support Windows.");
} else {
BetterFeedback.of(context).show(_launchFeedback);
}
}
break;
case _DrawerIndex.Help:
if (!LocalDatabaseService.preferences.developerMode) {
_screenView = const HelpScreen();
}
break;
case _DrawerIndex.About:
if (!LocalDatabaseService.preferences.developerMode) {
_screenView = const AboutPage();
}
break;
}
});
}
@override
Widget build(BuildContext context) {
final List<CustomDrawerItem> drawerItems = [
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.humanVsAi,
title: S.of(context).humanVsAi,
icon: const Icon(FluentIcons.person_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.humanVsHuman,
title: S.of(context).humanVsHuman,
icon: const Icon(FluentIcons.people_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.aiVsAi,
title: S.of(context).aiVsAi,
icon: const Icon(FluentIcons.bot_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.preferences,
title: S.of(context).preferences,
icon: const Icon(FluentIcons.options_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.ruleSettings,
title: S.of(context).ruleSettings,
icon: const Icon(FluentIcons.task_list_ltr_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.personalization,
title: S.of(context).personalization,
icon: const Icon(FluentIcons.design_ideas_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.feedback,
title: S.of(context).feedback,
icon: const Icon(FluentIcons.chat_warning_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.Help,
title: S.of(context).help,
icon: const Icon(FluentIcons.question_circle_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
CustomDrawerItem<_DrawerIndex>(
value: _DrawerIndex.About,
title: S.of(context).about,
icon: const Icon(FluentIcons.info_24_regular),
groupValue: _drawerIndex,
onChanged: _changeIndex,
),
];
// LocalDatabaseService.colorSettings.drawerColor,
return CustomDrawer(
controller: _controller,
header: CustomDrawerHeader(
title: S.of(context).appName,
),
items: drawerItems,
child: _screenView,
);
}
}
/// drafts an email and sends it to the developer
Future<void> _launchFeedback(UserFeedback feedback) async {
final screenshotFilePath = await _writeImageToStorage(feedback.screenshot);
final packageInfo = await PackageInfo.fromPlatform();
final _version = '${packageInfo.version} (${packageInfo.buildNumber})';
final Email email = Email(
body: feedback.text,
subject: Constants.feedbackSubjectPrefix +
_version +
Constants.feedbackSubjectSuffix,
recipients: [Constants.recipients],
attachmentPaths: [screenshotFilePath],
);
await FlutterEmailSender.send(email);
}
Future<String> _writeImageToStorage(Uint8List feedbackScreenshot) async {
final Directory output = await getTemporaryDirectory();
final String screenshotFilePath = '${output.path}/sanmill-feedback.png';
final File screenshotFile = File(screenshotFilePath);
await screenshotFile.writeAsBytes(feedbackScreenshot);
return screenshotFilePath;
}

View File

@ -1,261 +0,0 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of 'package:sanmill/screens/navigation_home_screen.dart';
enum DrawerIndex {
humanVsAi,
humanVsHuman,
aiVsAi,
preferences,
ruleSettings,
personalization,
feedback,
Help,
About
}
class DrawerListItem {
const DrawerListItem({
required this.index,
required this.title,
required this.icon,
});
final DrawerIndex index;
final String title;
final Icon icon;
}
class HomeDrawer extends StatelessWidget {
const HomeDrawer({
Key? key,
required this.screenIndex,
required this.iconAnimationController,
required this.callBackIndex,
required this.items,
}) : super(key: key);
final AnimationController iconAnimationController;
final DrawerIndex screenIndex;
final Function(DrawerIndex) callBackIndex;
final List<DrawerListItem> items;
@override
Widget build(BuildContext context) {
return Material(
color: LocalDatabaseService.colorSettings.drawerBackgroundColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_DrawerHeader(
iconAnimationController: iconAnimationController,
),
Divider(height: 1, color: AppTheme.drawerDividerColor),
ListView.builder(
padding: const EdgeInsets.only(top: 4.0),
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: items.length,
itemBuilder: _buildChildren,
),
//drawFooter,
],
),
);
}
Future<void> navigationToScreen(DrawerIndex index) async {
callBackIndex(index);
}
Widget _buildChildren(BuildContext context, int index) {
final listItem = items[index];
final bool isSelected = screenIndex == listItem.index;
final bool ltr = Directionality.of(context) == TextDirection.ltr;
const double radius = 28.0;
final animatedBuilder = AnimatedBuilder(
animation: iconAnimationController,
builder: (BuildContext context, Widget? child) {
return Transform(
transform: Matrix4.translationValues(
(MediaQuery.of(context).size.width * 0.75 - 64) *
(1.0 - iconAnimationController.value - 1.0),
0.0,
0.0,
),
child: child,
);
},
child: Container(
width: MediaQuery.of(context).size.width * 0.75 - 64,
height: 46,
decoration: BoxDecoration(
color: LocalDatabaseService.colorSettings.drawerHighlightItemColor,
borderRadius: BorderRadius.horizontal(
right: ltr ? const Radius.circular(radius) : Radius.zero,
left: !ltr ? const Radius.circular(radius) : Radius.zero,
),
),
),
);
final listItemIcon = Icon(
listItem.icon.icon,
color: isSelected
? LocalDatabaseService
.colorSettings.drawerTextColor // TODO: drawerHighlightTextColor
: LocalDatabaseService.colorSettings.drawerTextColor,
);
final child = Row(
children: <Widget>[
const SizedBox(height: 46.0, width: 6.0),
const Padding(
padding: EdgeInsets.all(4.0),
),
listItemIcon,
const Padding(
padding: EdgeInsets.all(4.0),
),
Text(
listItem.title,
style: TextStyle(
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
fontSize: LocalDatabaseService.display.fontSize,
color: isSelected
? LocalDatabaseService.colorSettings.drawerTextColor
// TODO: drawerHighlightTextColor
: LocalDatabaseService.colorSettings.drawerTextColor,
),
),
],
);
return InkWell(
splashColor: AppTheme.drawerSplashColor,
highlightColor: AppTheme.drawerHighlightColor,
onTap: () => navigationToScreen(listItem.index),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: isSelected
? Stack(
children: <Widget>[
child,
animatedBuilder,
],
)
: child,
),
);
}
}
class _DrawerHeader extends StatelessWidget {
const _DrawerHeader({
Key? key,
required this.iconAnimationController,
}) : super(key: key);
final AnimationController iconAnimationController;
static const String _tag = "[home_drawer]";
void _enableDeveloperMode() {
Temp.developerMode = true;
debugPrint("$_tag Developer mode enabled.");
}
@override
Widget build(BuildContext context) {
final List<Color> animatedTextsColors = [
LocalDatabaseService.colorSettings.drawerTextColor,
Colors.black,
Colors.blue,
Colors.yellow,
Colors.red,
LocalDatabaseService.colorSettings.darkBackgroundColor,
LocalDatabaseService.colorSettings.boardBackgroundColor,
LocalDatabaseService.colorSettings.drawerHighlightItemColor,
];
final rotationTransition = RotationTransition(
turns: AlwaysStoppedAnimation<double>(
Tween<double>(begin: 0.0, end: 24.0)
.animate(
CurvedAnimation(
parent: iconAnimationController,
curve: Curves.fastOutSlowIn,
),
)
.value /
360,
),
);
final scaleTransition = ScaleTransition(
scale: AlwaysStoppedAnimation<double>(
1.0 - (iconAnimationController.value) * 0.2,
),
child: rotationTransition,
);
final animatedBuilder = AnimatedBuilder(
animation: iconAnimationController,
builder: (_, __) => scaleTransition,
);
final animation = GestureDetector(
onDoubleTap: _enableDeveloperMode,
child: AnimatedTextKit(
animatedTexts: [
ColorizeAnimatedText(
S.of(context).appName,
textStyle: TextStyle(
fontSize: LocalDatabaseService.display.fontSize + 16,
fontWeight: FontWeight.w600,
),
colors: animatedTextsColors,
speed: const Duration(seconds: 3),
),
],
pause: const Duration(seconds: 3),
repeatForever: true,
stopPauseOnTap: true,
onTap: () => debugPrint("$_tag DoubleTap to enable developer mode."),
),
);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// TODO: can animatedBuilder be removed? does not appear in the widget tree
animatedBuilder,
Padding(
padding: EdgeInsets.only(top: isLargeScreen ? 30 : 8, left: 4),
child: ExcludeSemantics(child: animation),
),
],
),
);
}
}

View File

@ -1,150 +0,0 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'dart:typed_data';
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:feedback/feedback.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sanmill/generated/intl/l10n.dart';
import 'package:sanmill/mill/game.dart';
import 'package:sanmill/models/temporary.dart';
import 'package:sanmill/screens/about_page.dart';
import 'package:sanmill/screens/game_page/game_page.dart';
import 'package:sanmill/screens/game_settings/game_settings_page.dart';
import 'package:sanmill/screens/help_screen.dart';
import 'package:sanmill/screens/personalization_settings/personalization_settings_page.dart';
import 'package:sanmill/screens/rule_settings/rule_settings_page.dart';
import 'package:sanmill/services/engine/engine.dart';
import 'package:sanmill/services/enviornment_config.dart';
import 'package:sanmill/services/storage/storage.dart';
import 'package:sanmill/shared/constants.dart';
import 'package:sanmill/shared/theme/app_theme.dart';
part 'package:sanmill/screens/home_drawer.dart';
part 'package:sanmill/shared/drawer_controller.dart';
class NavigationHomeScreen extends StatefulWidget {
const NavigationHomeScreen({Key? key}) : super(key: key);
@override
_NavigationHomeScreenState createState() => _NavigationHomeScreenState();
}
class _NavigationHomeScreenState extends State<NavigationHomeScreen> {
late Widget screenView;
late DrawerIndex drawerIndex;
@override
void initState() {
drawerIndex = DrawerIndex.humanVsAi;
screenView = const GamePage(EngineType.humanVsAi);
super.initState();
}
@override
Widget build(BuildContext context) {
return Material(
color: AppTheme.navigationHomeScreenBackgroundColor,
child: DrawerController(
screenIndex: drawerIndex,
drawerWidth: MediaQuery.of(context).size.width * 0.75,
onDrawerCall: changeIndex,
// we replace screen view as
// we need on navigate starting screens
screenView: screenView,
),
);
}
/// callback from drawer for replace screen
/// as user need with passing DrawerIndex (Enum index)
void changeIndex(DrawerIndex index) {
if (drawerIndex == index && drawerIndex != DrawerIndex.feedback) {
return;
}
final drawerMap = {
DrawerIndex.humanVsAi: EngineType.humanVsAi,
DrawerIndex.humanVsHuman: EngineType.humanVsHuman,
DrawerIndex.aiVsAi: EngineType.aiVsAi,
};
drawerIndex = index;
// TODO: use switch case
final engineType = drawerMap[drawerIndex];
setState(() {
if (engineType != null) {
gameInstance.setWhoIsAi(engineType);
screenView = GamePage(engineType);
} else if (drawerIndex == DrawerIndex.preferences) {
screenView = GameSettingsPage();
} else if (drawerIndex == DrawerIndex.ruleSettings) {
screenView = const RuleSettingsPage();
} else if (drawerIndex == DrawerIndex.personalization) {
screenView = PersonalizationSettingsPage();
} else if (drawerIndex == DrawerIndex.feedback &&
!EnvironmentConfig.monkeyTest) {
if (Platform.isWindows) {
debugPrint("flutter_email_sender does not support Windows.");
//_launchFeedback();
} else {
BetterFeedback.of(context).show((feedback) async {
// draft an email and send to developer
final screenshotFilePath =
await writeImageToStorage(feedback.screenshot);
final packageInfo = await PackageInfo.fromPlatform();
final _version =
'${packageInfo.version} (${packageInfo.buildNumber})';
final Email email = Email(
body: feedback.text,
subject: Constants.feedbackSubjectPrefix +
_version +
Constants.feedbackSubjectSuffix,
recipients: [Constants.recipients],
attachmentPaths: [screenshotFilePath],
);
await FlutterEmailSender.send(email);
});
}
} else if (drawerIndex == DrawerIndex.Help &&
!EnvironmentConfig.monkeyTest) {
screenView = HelpScreen();
} else if (drawerIndex == DrawerIndex.About &&
!EnvironmentConfig.monkeyTest) {
screenView = AboutPage();
} else {
//do in your way......
}
});
}
Future<String> writeImageToStorage(Uint8List feedbackScreenshot) async {
final Directory output = await getTemporaryDirectory();
final String screenshotFilePath = '${output.path}/sanmill-feedback.png';
final File screenshotFile = File(screenshotFilePath);
await screenshotFile.writeAsBytes(feedbackScreenshot);
return screenshotFilePath;
}
}

View File

@ -4,6 +4,8 @@ import 'package:sanmill/services/storage/storage.dart';
import 'package:sanmill/shared/theme/app_theme.dart';
class HelpScreen extends StatelessWidget {
const HelpScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -43,6 +43,8 @@ part 'package:sanmill/screens/personalization_settings/point_width_slider.dart';
part 'package:sanmill/screens/personalization_settings/language_picker.dart';
class PersonalizationSettingsPage extends StatelessWidget {
const PersonalizationSettingsPage({Key? key}) : super(key: key);
void setBoardBorderLineWidth(BuildContext context) => showModalBottomSheet(
context: context,
builder: (_) => const _BoardBorderWidthSlider(),

View File

@ -0,0 +1,37 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/// Although marked as a library this package is tightly integrated into the app
library custom_drawer;
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:sanmill/generated/intl/l10n.dart';
import 'package:sanmill/models/temporary.dart';
import 'package:sanmill/services/storage/storage.dart';
import 'package:sanmill/shared/constants.dart';
import 'package:sanmill/shared/theme/app_theme.dart';
part 'src/controller.dart';
part 'src/header.dart';
part 'src/item.dart';
part 'src/value.dart';
part 'src/widget.dart';

View File

@ -0,0 +1,47 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of '../custom_drawer.dart';
/// Custom Drawer Controller
///
/// manages the [CustomDrawer] state
class CustomDrawerController extends ValueNotifier<CustomDrawerValue> {
/// Creates a controller with the initial drawer state (Hidden by default)
CustomDrawerController([CustomDrawerValue? value])
: super(value ?? CustomDrawerValue.hidden());
/// shows the drawer
void showDrawer() {
value = CustomDrawerValue.visible();
}
/// hides the drawer
void hideDrawer() {
value = CustomDrawerValue.hidden();
}
/// toggles the drawer visibility
void toggleDrawer() {
if (value.visible) {
hideDrawer();
} else {
showDrawer();
}
}
}

View File

@ -0,0 +1,84 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of '../custom_drawer.dart';
// TODO: [Leptopoda] maybe extend DrawerHeader
class CustomDrawerHeader extends StatelessWidget {
const CustomDrawerHeader({
Key? key,
required this.title,
}) : super(key: key);
final String title;
static const String _tag = "[home_drawer]";
void _enableDeveloperMode() {
Temp.developerMode = true;
debugPrint("$_tag Developer mode enabled.");
}
@override
Widget build(BuildContext context) {
final List<Color> _animatedTextsColors = [
LocalDatabaseService.colorSettings.drawerTextColor,
Colors.black,
Colors.blue,
Colors.yellow,
Colors.red,
LocalDatabaseService.colorSettings.darkBackgroundColor,
LocalDatabaseService.colorSettings.boardBackgroundColor,
LocalDatabaseService.colorSettings.drawerHighlightItemColor,
];
final animation = GestureDetector(
onDoubleTap: _enableDeveloperMode,
child: AnimatedTextKit(
animatedTexts: [
ColorizeAnimatedText(
title,
textStyle: TextStyle(
fontSize: LocalDatabaseService.display.fontSize + 16,
fontWeight: FontWeight.w600,
),
colors: _animatedTextsColors,
speed: const Duration(seconds: 3),
),
],
pause: const Duration(seconds: 3),
repeatForever: true,
stopPauseOnTap: true,
onTap: () => debugPrint("$_tag DoubleTap to enable developer mode."),
),
);
final _padding = EdgeInsets.only(
bottom: 16.0,
top: 16.0 + (isLargeScreen ? 30 : 8),
left: 20,
right: 16,
);
return Padding(
padding: _padding,
child: ExcludeSemantics(child: animation),
);
}
}

View File

@ -0,0 +1,75 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of '../custom_drawer.dart';
class CustomDrawerItem<T> extends StatelessWidget {
const CustomDrawerItem({
Key? key,
required this.groupValue,
required this.onChanged,
required this.value,
required this.title,
required this.icon,
}) : super(key: key);
final T groupValue;
final Function(T) onChanged;
final T value;
final String title;
final Icon icon;
bool get selected => groupValue == value;
@override
Widget build(BuildContext context) {
// TODO: drawerHighlightTextColor
final _color = selected
? LocalDatabaseService.colorSettings.drawerTextColor
: LocalDatabaseService.colorSettings.drawerTextColor;
final listItemIcon = Icon(
icon.icon,
color: _color,
);
final _drawerItem = Row(
children: <Widget>[
const SizedBox(height: 46.0, width: 6.0),
const Padding(padding: EdgeInsets.all(4.0)),
listItemIcon,
const Padding(padding: EdgeInsets.all(4.0)),
Text(
title,
style: TextStyle(
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
fontSize: LocalDatabaseService.display.fontSize,
color: _color,
),
),
],
);
return InkWell(
splashColor: AppTheme.drawerSplashColor,
highlightColor: AppTheme.drawerHighlightColor,
onTap: () => onChanged(value),
child: _drawerItem,
);
}
}

View File

@ -0,0 +1,43 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of '../custom_drawer.dart';
/// CustomDrawer Value
///
/// the different states athe [CustomDrawer] can be in
class CustomDrawerValue {
const CustomDrawerValue({
this.visible = false,
});
/// indicates whether drawer visible or not
final bool visible;
/// creates a value with hidden state
factory CustomDrawerValue.hidden() {
return const CustomDrawerValue();
}
/// creates a value with visible state
factory CustomDrawerValue.visible() {
return const CustomDrawerValue(
visible: true,
);
}
}

View File

@ -0,0 +1,288 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of '../custom_drawer.dart';
/// CustomDrawer Widget
///
/// The widget laying out the custom drawer
class CustomDrawer extends StatefulWidget {
const CustomDrawer({
Key? key,
required this.child,
required this.items,
required this.header,
this.controller,
this.disabledGestures = false,
}) : super(key: key);
/// Child widget. (Usually awidget that represents the main screen)
final Widget child;
/// controller that controls the widget state. By default a new controller will be geneerated.
final CustomDrawerController? controller;
/// disables the gestures.
final bool disabledGestures;
/// items the drawer holds
final List<CustomDrawerItem> items;
/// header widget of the drawer
final Widget header;
@override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer>
with SingleTickerProviderStateMixin {
// TODO: [Leptopoda] maybe integrate the animation controller into the drawerController
late final CustomDrawerController _controller;
late final AnimationController _animationController;
late final Animation<Offset> _childSlideAnimation;
late final Animation<Offset> _overlaySlideAnimation;
late double _offsetValue;
late Offset _freshPosition;
late Offset _startPosition;
bool _captured = false;
static const _duration = Duration(milliseconds: 250);
static const _slideThreshold = 0.5;
static const _openRatio = 0.75;
static const _overlayRadius = 28.0;
@override
void initState() {
super.initState();
_controller = widget.controller ?? CustomDrawerController();
_controller.addListener(_handleControllerChanged);
_animationController = AnimationController(
vsync: this,
duration: _duration,
value: _controller.value.visible ? 1 : 0,
);
_childSlideAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(_openRatio, 0),
).animate(
_animationController,
);
_overlaySlideAnimation = Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(
_animationController,
);
}
@override
Widget build(BuildContext context) {
final _drawer = Align(
alignment: AlignmentDirectional.centerStart,
child: FractionallySizedBox(
widthFactor: _openRatio,
child: Material(
color: LocalDatabaseService.colorSettings.drawerColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
widget.header,
Divider(height: 1, color: AppTheme.drawerDividerColor),
ListView.builder(
padding: const EdgeInsets.only(top: 4.0),
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: widget.items.length,
itemBuilder: _buildItem,
),
],
),
),
),
);
// TODO: [Leptopoda] move the drawer overlay into the main scaffold so we don't need to deal with positioning
final rtl = Directionality.of(context) == TextDirection.rtl;
/// menu and arrow icon animation overlay
final _drawerOverlay = IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.arrow_menu,
color: AppTheme.drawerAnimationIconColor,
progress: ReverseAnimation(_animationController),
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + (rtl ? 25 : 10),
),
tooltip: S.of(context).mainMenu,
onPressed: () => _controller.toggleDrawer(),
);
final _mainView = SlideTransition(
position: _childSlideAnimation,
textDirection: Directionality.of(context),
child: ValueListenableBuilder<CustomDrawerValue>(
valueListenable: _controller,
// TODO: [Leptopdoa] why isn't it working with GestureDetector?
builder: (_, value, child) => InkWell(
onTap: _controller.hideDrawer,
focusColor: Colors.transparent,
child: IgnorePointer(
ignoring: value.visible,
child: child,
),
),
child: DecoratedBox(
decoration: BoxDecoration(
color: LocalDatabaseService.colorSettings.drawerColor,
boxShadow: [
BoxShadow(
color: AppTheme.drawerBoxerShadowColor,
blurRadius: 24,
),
],
),
child: Stack(
children: <Widget>[
widget.child,
_drawerOverlay,
],
),
),
),
);
// TODO: [Leptopdoa] should the geture also apply to the drawer?
return Stack(
children: <Widget>[
_drawer,
GestureDetector(
onHorizontalDragStart:
widget.disabledGestures ? null : _handleDragStart,
onHorizontalDragUpdate:
widget.disabledGestures ? null : _handleDragUpdate,
onHorizontalDragEnd: widget.disabledGestures ? null : _handleDragEnd,
onHorizontalDragCancel:
widget.disabledGestures ? null : _handleDragCancel,
child: _mainView,
),
],
);
}
Widget _buildItem(BuildContext context, int index) {
final item = widget.items[index];
final Widget child;
if (item.selected) {
final overlay = SlideTransition(
position: _overlaySlideAnimation,
textDirection: Directionality.of(context),
child: Container(
width: MediaQuery.of(context).size.width * 0.75 - 64,
height: 46,
decoration: BoxDecoration(
color: LocalDatabaseService.colorSettings.drawerHighlightItemColor,
borderRadius: const BorderRadiusDirectional.horizontal(
end: Radius.circular(_overlayRadius),
),
),
),
);
child = Stack(
children: <Widget>[
overlay,
item,
],
);
} else {
child = item;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: child,
);
}
void _handleControllerChanged() {
_controller.value.visible
? _animationController.forward()
: _animationController.reverse();
}
void _handleDragStart(DragStartDetails details) {
_captured = true;
_startPosition = details.globalPosition;
_offsetValue = _animationController.value;
}
void _handleDragUpdate(DragUpdateDetails details) {
if (!_captured) return;
final screenSize = MediaQuery.of(context).size;
final rtl = Directionality.of(context) == TextDirection.rtl;
_freshPosition = details.globalPosition;
final diff = (_freshPosition - _startPosition).dx;
_animationController.value = _offsetValue +
(diff / (screenSize.width * _openRatio)) * (rtl ? -1 : 1);
}
void _handleDragEnd(DragEndDetails details) {
if (!_captured) return;
_captured = false;
if (_animationController.value >= _slideThreshold) {
if (_controller.value.visible) {
_animationController.forward();
} else {
_controller.showDrawer();
}
} else {
if (!_controller.value.visible) {
_animationController.reverse();
} else {
_controller.hideDrawer();
}
}
}
void _handleDragCancel() {
_captured = false;
}
@override
void dispose() {
_controller.removeListener(_handleControllerChanged);
_animationController.dispose();
_controller.dispose();
super.dispose();
}
}

View File

@ -1,312 +0,0 @@
/*
This file is part of Sanmill.
Copyright (C) 2019-2021 The Sanmill developers (see AUTHORS file)
Sanmill is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Sanmill is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
part of 'package:sanmill/screens/navigation_home_screen.dart';
class DrawerController extends StatefulWidget {
const DrawerController({
Key? key,
this.drawerWidth = AppTheme.drawerWidth,
required this.onDrawerCall,
required this.screenView,
this.animatedIconData = AnimatedIcons.arrow_menu,
this.menuView,
this.drawerIsOpen,
required this.screenIndex,
}) : super(key: key);
final double drawerWidth;
final Function(DrawerIndex) onDrawerCall;
final Widget screenView;
final Function(bool)? drawerIsOpen;
final AnimatedIconData animatedIconData;
final Widget? menuView;
final DrawerIndex screenIndex;
@override
_DrawerControllerState createState() => _DrawerControllerState();
}
class _DrawerControllerState extends State<DrawerController>
with TickerProviderStateMixin {
late final ScrollController scrollController;
late final AnimationController iconAnimationController;
late final AnimationController animationController;
double scrollOffset = 0.0;
@override
void initState() {
animationController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
iconAnimationController =
AnimationController(vsync: this, duration: Duration.zero);
iconAnimationController.animateTo(
1.0,
duration: Duration.zero,
curve: Curves.fastOutSlowIn,
);
scrollController =
ScrollController(initialScrollOffset: widget.drawerWidth);
scrollController.addListener(() {
if (scrollController.offset <= 0) {
if (scrollOffset != 1.0) {
setState(() {
scrollOffset = 1.0;
try {
widget.drawerIsOpen!(true);
} catch (_) {}
});
}
iconAnimationController.animateTo(
0.0,
duration: Duration.zero,
curve: Curves.fastOutSlowIn,
);
} else if (scrollController.offset < widget.drawerWidth.floor()) {
iconAnimationController.animateTo(
(scrollController.offset * 100 / (widget.drawerWidth)) / 100,
duration: Duration.zero,
curve: Curves.fastOutSlowIn,
);
} else {
if (scrollOffset != 0.0) {
setState(() {
scrollOffset = 0.0;
try {
widget.drawerIsOpen!(false);
} catch (_) {}
});
}
iconAnimationController.animateTo(
1.0,
duration: Duration.zero,
curve: Curves.fastOutSlowIn,
);
}
});
WidgetsBinding.instance!.addPostFrameCallback((_) => getInitState());
super.initState();
}
bool getInitState() {
scrollController.jumpTo(
widget.drawerWidth,
);
return true;
}
@override
Widget build(BuildContext context) {
final bool ltr = Directionality.of(context) == TextDirection.ltr;
// this just menu and arrow icon animation
final inkWell = InkWell(
borderRadius: BorderRadius.circular(AppBar().preferredSize.height),
child: Center(
// if you use your own menu view UI you add form initialization
child: widget.menuView ??
Semantics(
label: S.of(context).mainMenu,
child: AnimatedIcon(
icon: widget.animatedIconData,
color: AppTheme.drawerAnimationIconColor,
progress: iconAnimationController,
),
),
),
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
onDrawerClick();
},
);
final List<DrawerListItem> drawerItems = [
DrawerListItem(
index: DrawerIndex.humanVsAi,
title: S.of(context).humanVsAi,
icon: const Icon(FluentIcons.person_24_regular),
),
DrawerListItem(
index: DrawerIndex.humanVsHuman,
title: S.of(context).humanVsHuman,
icon: const Icon(FluentIcons.people_24_regular),
),
DrawerListItem(
index: DrawerIndex.aiVsAi,
title: S.of(context).aiVsAi,
icon: const Icon(FluentIcons.bot_24_regular),
),
DrawerListItem(
index: DrawerIndex.preferences,
title: S.of(context).preferences,
icon: const Icon(FluentIcons.options_24_regular),
),
DrawerListItem(
index: DrawerIndex.ruleSettings,
title: S.of(context).ruleSettings,
icon: const Icon(FluentIcons.task_list_ltr_24_regular),
),
DrawerListItem(
index: DrawerIndex.personalization,
title: S.of(context).personalization,
icon: const Icon(FluentIcons.design_ideas_24_regular),
),
DrawerListItem(
index: DrawerIndex.feedback,
title: S.of(context).feedback,
icon: const Icon(FluentIcons.chat_warning_24_regular),
),
DrawerListItem(
index: DrawerIndex.Help,
title: S.of(context).help,
icon: const Icon(FluentIcons.question_circle_24_regular),
),
DrawerListItem(
index: DrawerIndex.About,
title: S.of(context).about,
icon: const Icon(FluentIcons.info_24_regular),
),
];
final animatedBuilder = AnimatedBuilder(
animation: iconAnimationController,
builder: (BuildContext context, Widget? child) {
return Transform(
// transform we use for the stable drawer
// we not need to move with scroll view
transform:
Matrix4.translationValues(scrollController.offset, 0.0, 0.0),
child: HomeDrawer(
screenIndex: widget.screenIndex,
iconAnimationController: iconAnimationController,
callBackIndex: (DrawerIndex indexType) {
onDrawerClick();
try {
widget.onDrawerCall(indexType);
} catch (_) {}
},
items: drawerItems,
),
);
},
);
var tapOffset = 0;
if (!ltr) {
tapOffset = 10; // TODO: WAR
}
final stack = Stack(
children: <Widget>[
// this IgnorePointer we use as touch(user Interface) widget.screen View,
// for example scrolloffset == 1
// means drawer is close we just allow touching all widget.screen View
IgnorePointer(
ignoring: scrollOffset == 1 || false,
child: widget.screenView,
),
// alternative touch(user Interface) for widget.screen,
// for example, drawer is close we need to
// tap on a few home screen area and close the drawer
if (scrollOffset == 1.0)
InkWell(
onTap: onDrawerClick,
),
Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + tapOffset,
),
child: SizedBox(
width: kToolbarHeight,
height: kToolbarHeight,
child: Material(
color: Colors.transparent,
child: inkWell,
),
),
),
],
);
final row = Row(
children: <Widget>[
SizedBox(
width: widget.drawerWidth,
// we divided first drawer Width with HomeDrawer
// and second full-screen Width with all home screen,
// we called screen View
height: MediaQuery.of(context).size.height,
child: animatedBuilder,
),
SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
//full-screen Width with widget.screenView
child: Container(
decoration: BoxDecoration(
color: LocalDatabaseService.colorSettings.drawerColor,
boxShadow: <BoxShadow>[
BoxShadow(
color: AppTheme.drawerBoxerShadowColor,
blurRadius: 24,
),
],
),
child: stack,
),
),
],
);
return Material(
color: LocalDatabaseService.colorSettings.drawerColor,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
physics: const PageScrollPhysics(parent: ClampingScrollPhysics()),
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width + widget.drawerWidth,
// we use with as screen width and add drawerWidth
// (from navigation_home_screen)
child: row,
),
),
);
}
void onDrawerClick() {
// if scrollController.offset != 0.0
// then we set to closed the drawer(with animation to offset zero position)
// if is not 1 then open the drawer
scrollController.animateTo(
scrollController.offset == 0.0 ? widget.drawerWidth : 0.0,
duration: const Duration(milliseconds: 400),
curve: Curves.fastOutSlowIn,
);
}
}

View File

@ -19,7 +19,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sanmill/generated/intl/l10n.dart';
import 'package:sanmill/screens/navigation_home_screen.dart';
import 'package:sanmill/screens/home.dart';
void main() {
Widget makeTestableWidget({required Widget child, required Locale locale}) {
@ -32,7 +32,7 @@ void main() {
}
testWidgets('Widget', (WidgetTester tester) async {
const _screen = NavigationHomeScreen();
const _screen = Home();
await tester.pumpWidget(
makeTestableWidget(
child: _screen,