From d1df5422c0bc5901737eb7097904b6e04926ab2c Mon Sep 17 00:00:00 2001 From: Ryunosuke Muramatsu Date: Thu, 29 Aug 2024 10:49:31 +0900 Subject: [PATCH] refactor: Merge appbar and body files into page --- .../chat_ui/chat_app_conversation_page.dart | 118 ------ .../cookbook/chat_ui/chat_message_bubble.dart | 339 ------------------ .../lib/pages/account/account_page.dart | 66 +++- .../components/account_page_app_bar.dart | 19 - .../account/components/account_page_body.dart | 55 --- .../pages/app_info_page/app_info_page.dart | 64 +++- .../components/app_info_app_bar.dart | 19 - .../components/app_info_body.dart | 51 --- .../components/not_found_app_bar.dart | 19 - .../components/not_found_page_body.dart | 37 -- .../lib/pages/not_found_page/error_page.dart | 55 ++- .../components/pinkie_mew_page_app_bar.dart | 20 -- .../components/pinkie_mew_page_body.dart | 11 - .../lib/pages/pinkie_mew/pinkie_mew_page.dart | 17 - .../components/settings_page_app_bar.dart | 20 -- .../components/settings_page_body.dart | 38 -- .../pages/settings_page/settings_page.dart | 44 ++- .../theme_selection_page_app_bar.dart | 30 -- .../components/theme_selection_page_body.dart | 31 -- .../theme_selection_page.dart | 57 ++- .../lib/src/altive_theme/altive_theme.dart | 1 - .../themes/lib/src/altive_theme/layout.dart | 3 - 22 files changed, 260 insertions(+), 854 deletions(-) delete mode 100644 packages/flutter_app/lib/features/cookbook/chat_ui/chat_app_conversation_page.dart delete mode 100644 packages/flutter_app/lib/features/cookbook/chat_ui/chat_message_bubble.dart delete mode 100644 packages/flutter_app/lib/pages/account/components/account_page_app_bar.dart delete mode 100644 packages/flutter_app/lib/pages/account/components/account_page_body.dart delete mode 100644 packages/flutter_app/lib/pages/app_info_page/components/app_info_app_bar.dart delete mode 100644 packages/flutter_app/lib/pages/app_info_page/components/app_info_body.dart delete mode 100644 packages/flutter_app/lib/pages/not_found_page/components/not_found_app_bar.dart delete mode 100644 packages/flutter_app/lib/pages/not_found_page/components/not_found_page_body.dart delete mode 100644 packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_app_bar.dart delete mode 100644 packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_body.dart delete mode 100644 packages/flutter_app/lib/pages/pinkie_mew/pinkie_mew_page.dart delete mode 100644 packages/flutter_app/lib/pages/settings_page/components/settings_page_app_bar.dart delete mode 100644 packages/flutter_app/lib/pages/settings_page/components/settings_page_body.dart delete mode 100644 packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_app_bar.dart delete mode 100644 packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_body.dart delete mode 100644 packages/themes/lib/src/altive_theme/layout.dart diff --git a/packages/flutter_app/lib/features/cookbook/chat_ui/chat_app_conversation_page.dart b/packages/flutter_app/lib/features/cookbook/chat_ui/chat_app_conversation_page.dart deleted file mode 100644 index 93e68a80..00000000 --- a/packages/flutter_app/lib/features/cookbook/chat_ui/chat_app_conversation_page.dart +++ /dev/null @@ -1,118 +0,0 @@ -// Citation: https://gist.github.com/craiglabenz/c6fc52e3e61f66c51f7a858115bfce51 -import 'package:convenient_widgets/convenient_widgets.dart'; -import 'package:flutter/material.dart'; - -import 'chat_message_bubble.dart'; - -/// A full-screen widget which vaguely resembles a chat app's conversation view. -/// -/// The main purpose of the [ChatAppConversationPage] class is to feed the value -/// from a [TextEditingController] into a [ChatMessageBubble] widget. The -/// [ChatMessageBubble] is the real star of the show. -class ChatAppConversationPage extends StatefulWidget { - const ChatAppConversationPage({super.key}); - - @override - State createState() => - _ChatAppConversationPageState(); -} - -class _ChatAppConversationPageState extends State { - final TextEditingController _controller = TextEditingController(); - final String sentAt = '3 seconds ago'; - - final List _messages = [ - '''Hello?! this is a message! If you read it for long enough, your brain will grow''', - ]; - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return UnfocusOnTap( - child: Scaffold( - appBar: AppBar( - title: const Text('Chat App Conversation'), - ), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: _messages.length, - itemBuilder: (context, index) { - final message = _messages[index]; - return Align( - alignment: Alignment.centerRight, - child: LayoutBuilder( - builder: (context, constraints) { - return Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - constraints: BoxConstraints( - maxWidth: constraints.maxWidth * 0.75, - ), - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(8)), - color: Theme.of(context).colorScheme.primary, - ), - child: ChatMessageBubble( - text: message, - sentAt: sentAt, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ); - }, - ), - ); - }, - ), - ), - TextField( - controller: _controller, - minLines: 1, - maxLines: 10, - decoration: InputDecoration( - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.zero, - ), - contentPadding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - hintText: 'Type a message', - suffixIcon: IconButton( - onPressed: () { - if (_controller.text.isEmpty) { - return; - } - setState(() { - _messages.add(_controller.text); - _controller.clear(); - }); - }, - icon: const Icon(Icons.send), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/packages/flutter_app/lib/features/cookbook/chat_ui/chat_message_bubble.dart b/packages/flutter_app/lib/features/cookbook/chat_ui/chat_message_bubble.dart deleted file mode 100644 index 02611e9f..00000000 --- a/packages/flutter_app/lib/features/cookbook/chat_ui/chat_message_bubble.dart +++ /dev/null @@ -1,339 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -/// Simplified variant of the [Text] widget which accepts both a primary string -/// for the raw text body, and a secondary `sentAt` string which annotates the -/// former with a timestamp, similar to how popular chat apps like WhatsApp -/// render individual messages. -/// -/// The [ChatMessageBubble] extends [LeafRenderObjectWidget], which means -/// it has no children and instead creates a -/// [ChatMessageRenderObject], -/// which handles all layout and painting itself. -class ChatMessageBubble extends LeafRenderObjectWidget { - const ChatMessageBubble({ - super.key, - required this.text, - required this.sentAt, - this.style, - }); - - final String text; - final String sentAt; - final TextStyle? style; - - @override - RenderObject createRenderObject(BuildContext context) { - final defaultTextStyle = DefaultTextStyle.of(context); - var effectiveTextStyle = style; - if (style == null || style!.inherit) { - effectiveTextStyle = defaultTextStyle.style.merge(style); - } - return ChatMessageRenderObject( - text: text, - sentAt: sentAt, - textDirection: Directionality.of(context), - textStyle: effectiveTextStyle!, - sentAtStyle: effectiveTextStyle.copyWith( - color: Theme.of(context).colorScheme.onPrimary.withOpacity(0.5), - ), - ); - } - - @override - void updateRenderObject( - BuildContext context, - ChatMessageRenderObject renderObject, - ) { - final defaultTextStyle = DefaultTextStyle.of(context); - var effectiveTextStyle = style; - if (style == null || style!.inherit) { - effectiveTextStyle = defaultTextStyle.style.merge(style); - } - renderObject - ..text = text - ..textStyle = effectiveTextStyle! - ..sentAt = sentAt - ..textDirection = Directionality.of(context); - } -} - -/// Simplified variant of [RenderParagraph] which supports the -/// [ChatMessageBubble] widget. -/// -/// Like the [Text] widget and its inner [RenderParagraph], the -/// [ChatMessageRenderObject] makes heavy use of the [TextPainter] -/// class. -class ChatMessageRenderObject extends RenderBox { - ChatMessageRenderObject({ - required String sentAt, - required String text, - required TextStyle sentAtStyle, - required TextStyle textStyle, - required TextDirection textDirection, - }) { - _text = text; - _sentAt = sentAt; - _textStyle = textStyle; - _sentAtStyle = sentAtStyle; - _textDirection = textDirection; - _textPainter = TextPainter( - text: TextSpan(text: _text, style: _textStyle), - textDirection: _textDirection, - ); - _sentAtTextPainter = TextPainter( - text: TextSpan(text: _sentAt, style: _sentAtStyle), - textDirection: _textDirection, - ); - } - - late TextDirection _textDirection; - late String _text; - late String _sentAt; - late TextPainter _textPainter; - late TextPainter _sentAtTextPainter; - late TextStyle _sentAtStyle; - late TextStyle _textStyle; - late bool _sentAtFitsOnLastLine; - late double _lineHeight; - late double _lastMessageLineWidth; - double _longestLineWidth = 0; - late double _sentAtLineWidth; - late int _numMessageLines; - - String get sentAt => _sentAt; - set sentAt(String val) { - _sentAt = val; - // `sentAtTextSpan` is a computed property that incorporates both the raw - // string value and the [TextStyle], - // so we have to update the whole [TextSpan] - // any time either value is updated. - _sentAtTextPainter.text = sentAtTextSpan; - markNeedsLayout(); - - // Because changing any text in our widget will definitely change the - // semantic meaning of this piece of our UI, we need to call - markNeedsSemanticsUpdate(); - } - - TextStyle get sentAtStyle => _sentAtStyle; - set sentAtStyle(TextStyle val) { - _sentAtStyle = val; - // `sentAtTextSpan` is a computed property that incorporates both the raw - // string value and the [TextStyle], - // so we have to update the whole [TextSpan] - // any time either value is updated. - _sentAtTextPainter.text = sentAtTextSpan; - markNeedsLayout(); - } - - String get text => _text; - set text(String val) { - _text = val; - _textPainter.text = textTextSpan; - markNeedsLayout(); - // Because changing any text in our widget will definitely change the - // semantic meaning of this piece of our UI, we need to call - markNeedsSemanticsUpdate(); - } - - TextStyle get textStyle => _textStyle; - set textStyle(TextStyle val) { - _textStyle = val; - _textPainter.text = textTextSpan; - - // If we knew that the new [TextStyle] had only changed in certain ways - // (e.g. color) then we could be more performant and call `markNeedsPaint()` - // instead. - // However, without carefully making that assessment, it is safer to call - // the more generic method, `markNeedsLayout()` instead (which also implies - // a repaint). - markNeedsLayout(); - } - - TextDirection get textDirection => _textDirection; - set textDirection(TextDirection val) { - if (_textDirection == val) { - return; - } - _textPainter.textDirection = val; - _sentAtTextPainter.textDirection = val; - markNeedsSemanticsUpdate(); - } - - TextSpan get textTextSpan => TextSpan(text: _text, style: _textStyle); - TextSpan get sentAtTextSpan => TextSpan(text: _sentAt, style: _sentAtStyle); - - @override - void describeSemanticsConfiguration(SemanticsConfiguration config) { - super.describeSemanticsConfiguration(config); - // Set this to `true` because individual chat bubbles are perfectly - // self-contained semantic objects. - config - ..isSemanticBoundary = true - ..label = '$_text, sent $_sentAt' - ..textDirection = _textDirection; - } - - @override - double computeMinIntrinsicWidth(double height) { - // Ignore `height` parameter because chat bubbles' height grows as a - // function of available width and text length. - - _layoutText(double.infinity); - return _longestLineWidth; - } - - @override - double computeMinIntrinsicHeight(double width) => - computeMaxIntrinsicHeight(width); - - @override - double computeMaxIntrinsicHeight(double width) { - final computedSize = _layoutText(width); - return computedSize.height; - } - - @override - void performLayout() { - final unconstrainedSize = _layoutText(constraints.maxWidth); - size = constraints.constrain( - Size(unconstrainedSize.width, unconstrainedSize.height), - ); - } - - /// Lays out the text within a given width constraint and returns its [Size]. - /// - /// Because [_layoutText] is called from multiple places with multiple - /// concerns, - /// like intrinsics which could have different width parameters than a typical - /// layout, this logic is moved out of `performLayout` and into a dedicated - /// method which accepts and works with any width constraint. - Size _layoutText(double maxWidth) { - // Draw nothing (requiring no size) if the string doesn't exist. This is one - // of many opinionated choices we could make here if the text is empty. - if (_textPainter.text?.toPlainText() == '') { - return Size.zero; - } - assert( - maxWidth > 0, - 'You must allocate SOME space to layout a ' - 'TimestampedChatMessageRenderObject. Received a ' - '`maxWidth` value of $maxWidth.', - ); - - // Layout the raw message, which saves expected high-level sizing values - // to the painter itself. - _textPainter.layout(maxWidth: maxWidth); - final textLines = _textPainter.computeLineMetrics(); - - // Now make similar calculations for `sentAt`. - _sentAtTextPainter.layout(maxWidth: maxWidth); - _sentAtLineWidth = _sentAtTextPainter.computeLineMetrics().first.width; - - // Reset cached values from the last frame if they're assumed to start at 0. - // (Because this is used in `max`, if it opens a new frame still holding the - // value from a previous frame, we could fail to accurately calculate the - // longest line.) - _longestLineWidth = 0; - - // Next, we calculate a few metrics for the height and width of the message. - - // First, chat messages don't actually grow to their full available width - // if their longest line does not require it. Thus, we need to note the - // longest line in the message. - for (final line in textLines) { - _longestLineWidth = max(_longestLineWidth, line.width); - } - // If the message is very short, it's possible that the longest line is - // is actually the date. - _longestLineWidth = max(_longestLineWidth, _sentAtTextPainter.width); - - // Because [_textPainter.width] can be the maximum width we passed to it, - // even if the longest line is shorter, we use this logic to determine its - // real size, for our purposes. - final sizeOfMessage = Size(_longestLineWidth, _textPainter.height); - - // Cache additional variables used both in the rest of this method and in - // `paint` later on. - _lastMessageLineWidth = textLines.last.width; - _lineHeight = textLines.last.height; - _numMessageLines = textLines.length; - - // Determine whether the message's last line and the date can share a - // horizontal row together. - final lastLineWithDate = _lastMessageLineWidth + (_sentAtLineWidth * 1.08); - if (textLines.length == 1) { - _sentAtFitsOnLastLine = lastLineWithDate < maxWidth; - } else { - _sentAtFitsOnLastLine = - lastLineWithDate < min(_longestLineWidth, maxWidth); - } - - late Size computedSize; - if (!_sentAtFitsOnLastLine) { - computedSize = Size( - // If `sentAt` does not fit on the longest line, then we know the - // message contains a long line, making this a safe value for `width`. - sizeOfMessage.width, - // And similarly, if `sentAt` does not fit, we know to add its height - // to the overall size of just-the-message. - sizeOfMessage.height + _sentAtTextPainter.height, - ); - } else { - // Moving forward, of course, we know that `sentAt` DOES fit into the last - // line. - - if (textLines.length == 1) { - computedSize = Size( - // When there is only 1 line, our width calculations are in a special - // case of needing as many pixels as our line plus the date, - // as opposed to the full size of the longest line. - lastLineWithDate, - sizeOfMessage.height, - ); - } else { - computedSize = Size( - // But when there's more than 1 line, our width should be equal to the - // longest line. - _longestLineWidth, - sizeOfMessage.height, - ); - } - } - return computedSize; - } - - @override - void paint(PaintingContext context, Offset offset) { - // Draw nothing (requiring no paint calls) if the string doesn't exist. - // This is one of many opinionated choices we could make here if the text is - // empty. - if (_textPainter.text?.toPlainText() == '') { - return; - } - - // This line writes the actual message to the screen. Because we use the - // same offset we were passed, the text will appear in the upper-left corner - // of our available space. - _textPainter.paint(context.canvas, offset); - - late Offset sentAtOffset; - if (_sentAtFitsOnLastLine) { - sentAtOffset = Offset( - offset.dx + (size.width - _sentAtLineWidth), - offset.dy + (_lineHeight * (_numMessageLines - 1)), - ); - } else { - sentAtOffset = Offset( - offset.dx + (size.width - _sentAtLineWidth), - offset.dy + _lineHeight * _numMessageLines, - ); - } - - // Finally, place the `sentAt` value accordingly. - _sentAtTextPainter.paint(context.canvas, sentAtOffset); - } -} diff --git a/packages/flutter_app/lib/pages/account/account_page.dart b/packages/flutter_app/lib/pages/account/account_page.dart index ee34af0d..4876bf29 100644 --- a/packages/flutter_app/lib/pages/account/account_page.dart +++ b/packages/flutter_app/lib/pages/account/account_page.dart @@ -1,16 +1,72 @@ +import 'package:awaitable_button/awaitable_button.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'components/account_page_app_bar.dart'; -import 'components/account_page_body.dart'; +import '../../gen/strings.g.dart'; +import '../../package_adaptor/authenticator_provider.dart'; +/// A page for account management. class AccountPage extends StatelessWidget { + /// Creates a new instance of [AccountPage]. const AccountPage({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - appBar: AccountPageAppBar(), - body: AccountPageBody(), + final t = Translations.of(context); + return Scaffold( + appBar: AppBar( + title: Text(t.accountPage.appBar.title), + ), + body: const _Body(), + ); + } +} + +class _Body extends HookConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = Translations.of(context); + final authenticator = ref.watch(authenticatorProvider); + Future signOut() async { + final isBool = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Sign out'), + content: const Text('Really?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(t.button.cancel), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(t.button.sign.out), + ), + ], + ); + }, + ); + if (isBool != true) { + return; + } + await authenticator.signOut(); + } + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + ListTile( + title: const Text('UID:'), + subtitle: Text(ref.watch(userProvider).valueOrNull?.uid ?? 'none'), + ), + AwaitableTextButton( + onPressed: signOut, + child: Text(t.button.sign.out), + ), + ], ); } } diff --git a/packages/flutter_app/lib/pages/account/components/account_page_app_bar.dart b/packages/flutter_app/lib/pages/account/components/account_page_app_bar.dart deleted file mode 100644 index 257310e6..00000000 --- a/packages/flutter_app/lib/pages/account/components/account_page_app_bar.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:themes/themes.dart'; - -import '../../../gen/strings.g.dart'; - -class AccountPageAppBar extends StatelessWidget implements PreferredSizeWidget { - const AccountPageAppBar({super.key}); - - @override - Size get preferredSize => appBarDefaultSize; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return AppBar( - title: Text(t.accountPage.appBar.title), - ); - } -} diff --git a/packages/flutter_app/lib/pages/account/components/account_page_body.dart b/packages/flutter_app/lib/pages/account/components/account_page_body.dart deleted file mode 100644 index 39f63bb3..00000000 --- a/packages/flutter_app/lib/pages/account/components/account_page_body.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:awaitable_button/awaitable_button.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../../gen/strings.g.dart'; -import '../../../package_adaptor/authenticator_provider.dart'; - -class AccountPageBody extends HookConsumerWidget { - const AccountPageBody({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = Translations.of(context); - final authenticator = ref.watch(authenticatorProvider); - Future signOut() async { - final isBool = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Sign out'), - content: const Text('Really?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(t.button.cancel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(t.button.sign.out), - ), - ], - ); - }, - ); - if (isBool != true) { - return; - } - await authenticator.signOut(); - } - - return ListView( - padding: const EdgeInsets.all(16), - children: [ - ListTile( - title: const Text('UID:'), - subtitle: Text(ref.watch(userProvider).valueOrNull?.uid ?? 'none'), - ), - AwaitableTextButton( - onPressed: signOut, - child: Text(t.button.sign.out), - ), - ], - ); - } -} diff --git a/packages/flutter_app/lib/pages/app_info_page/app_info_page.dart b/packages/flutter_app/lib/pages/app_info_page/app_info_page.dart index a4d354e6..99b9c1a2 100644 --- a/packages/flutter_app/lib/pages/app_info_page/app_info_page.dart +++ b/packages/flutter_app/lib/pages/app_info_page/app_info_page.dart @@ -1,23 +1,75 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'components/app_info_app_bar.dart'; -import 'components/app_info_body.dart'; +import '../../environment/environment.dart'; +import '../../gen/strings.g.dart'; +import '../../package_adaptor/package_info_provider.dart'; +import '../../widgets/widgets.dart'; +/// A page for app information. class AppInfoPage extends StatelessWidget { + /// Creates a new instance of [AppInfoPage]. const AppInfoPage({ super.key, }); @override Widget build(BuildContext context) { - return const Scaffold( - appBar: AppInfoAppBar(), - body: SafeArea( + final t = Translations.of(context); + return Scaffold( + appBar: AppBar( + title: Text(t.appInfo.appBar.title), + ), + body: const SafeArea( child: Padding( padding: EdgeInsets.all(16), - child: AppInfoBody(), + child: _Body(), ), ), ); } } + +class _Body extends HookConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = Translations.of(context); + final flavor = ref.watch(flavorProvider); + final packageInfo = ref.watch(packageInfoProvider); + return ListView( + children: [ + Text(t.title), + const Gap(8), + Text('Author is ${t.author.name}.'), + const Gap(8), + const Text('Developed by Ryunosuke Muramatsu.'), + const Divider(), + const Center(child: DisplayMediumText('App Environment')), + const Gap(16), + const TitleLargeText('Flavor'), + Text(flavor.name), + const Gap(32), + const TitleLargeText('App name'), + Text(packageInfo.appName), + const Gap(32), + const TitleLargeText('Package name'), + Text(packageInfo.packageName), + const Gap(32), + const TitleLargeText('Mode'), + const Text( + kReleaseMode + ? 'Release' + : kProfileMode + ? 'Profile' + : kDebugMode + ? 'Debug' + : 'unknown', + ), + ], + ); + } +} diff --git a/packages/flutter_app/lib/pages/app_info_page/components/app_info_app_bar.dart b/packages/flutter_app/lib/pages/app_info_page/components/app_info_app_bar.dart deleted file mode 100644 index 6d88895c..00000000 --- a/packages/flutter_app/lib/pages/app_info_page/components/app_info_app_bar.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:themes/themes.dart'; - -import '../../../gen/strings.g.dart'; - -class AppInfoAppBar extends StatelessWidget implements PreferredSizeWidget { - const AppInfoAppBar({super.key}); - - @override - Size get preferredSize => appBarDefaultSize; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return AppBar( - title: Text(t.appInfo.appBar.title), - ); - } -} diff --git a/packages/flutter_app/lib/pages/app_info_page/components/app_info_body.dart b/packages/flutter_app/lib/pages/app_info_page/components/app_info_body.dart deleted file mode 100644 index 52b391eb..00000000 --- a/packages/flutter_app/lib/pages/app_info_page/components/app_info_body.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../../environment/environment.dart'; -import '../../../gen/strings.g.dart'; -import '../../../package_adaptor/package_info_provider.dart'; -import '../../../widgets/widgets.dart'; - -class AppInfoBody extends HookConsumerWidget { - const AppInfoBody({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = Translations.of(context); - final flavor = ref.watch(flavorProvider); - final packageInfo = ref.watch(packageInfoProvider); - return ListView( - children: [ - Text(t.title), - const Gap(8), - Text('Author is ${t.author.name}.'), - const Gap(8), - const Text('Developed by Ryunosuke Muramatsu.'), - const Divider(), - const Center(child: DisplayMediumText('App Environment')), - const Gap(16), - const TitleLargeText('Flavor'), - Text(flavor.name), - const Gap(32), - const TitleLargeText('App name'), - Text(packageInfo.appName), - const Gap(32), - const TitleLargeText('Package name'), - Text(packageInfo.packageName), - const Gap(32), - const TitleLargeText('Mode'), - const Text( - kReleaseMode - ? 'Release' - : kProfileMode - ? 'Profile' - : kDebugMode - ? 'Debug' - : 'unknown', - ), - ], - ); - } -} diff --git a/packages/flutter_app/lib/pages/not_found_page/components/not_found_app_bar.dart b/packages/flutter_app/lib/pages/not_found_page/components/not_found_app_bar.dart deleted file mode 100644 index 8b5b60a5..00000000 --- a/packages/flutter_app/lib/pages/not_found_page/components/not_found_app_bar.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:themes/themes.dart'; - -import '../../../gen/strings.g.dart'; - -class NotFoundAppBar extends StatelessWidget implements PreferredSizeWidget { - const NotFoundAppBar({super.key}); - - @override - Size get preferredSize => appBarDefaultSize; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return AppBar( - title: Text(t.notFoundPage.title), - ); - } -} diff --git a/packages/flutter_app/lib/pages/not_found_page/components/not_found_page_body.dart b/packages/flutter_app/lib/pages/not_found_page/components/not_found_page_body.dart deleted file mode 100644 index a990ae48..00000000 --- a/packages/flutter_app/lib/pages/not_found_page/components/not_found_page_body.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../../gen/strings.g.dart'; -import '../../../router/router.dart'; -import '../../../widgets/widgets.dart'; - -class NotFoundPageBody extends HookConsumerWidget { - const NotFoundPageBody({ - super.key, - required this.path, - }); - - final String path; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = Translations.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.notFoundPage.header.label), - const Gap(32), - DisplaySmallText(path), - const Gap(32), - Text(t.notFoundPage.description), - Center( - child: ElevatedButton( - onPressed: () => const HomeRouteData().go(context), - child: Text(t.notFoundPage.backButton.label), - ), - ), - ], - ); - } -} diff --git a/packages/flutter_app/lib/pages/not_found_page/error_page.dart b/packages/flutter_app/lib/pages/not_found_page/error_page.dart index 57f87b10..adbce4ed 100644 --- a/packages/flutter_app/lib/pages/not_found_page/error_page.dart +++ b/packages/flutter_app/lib/pages/not_found_page/error_page.dart @@ -1,13 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'components/not_found_app_bar.dart'; -import 'components/not_found_page_body.dart'; +import '../../gen/strings.g.dart'; +import '../../router/router.dart'; +import '../../widgets/widgets.dart'; +/// A route for error. class ErrorRoute extends GoRouteData { - ErrorRoute({required this.location, required this.exception}); + /// Creates a new instance of [ErrorRoute]. + const ErrorRoute({required this.location, required this.exception}); + /// Location of the route. final String location; + + /// Exception. final Exception exception; @override @@ -17,24 +25,61 @@ class ErrorRoute extends GoRouteData { ); } +/// A page for error. class ErrorPage extends StatelessWidget { + /// Creates a new instance of [ErrorPage]. const ErrorPage({ super.key, required this.location, required this.exception, }); + /// Location of the route. final String location; + + /// Exception. final Exception exception; @override Widget build(BuildContext context) { + final t = Translations.of(context); return Scaffold( - appBar: const NotFoundAppBar(), + appBar: AppBar( + title: Text(t.notFoundPage.title), + ), body: Padding( padding: const EdgeInsets.all(16), - child: NotFoundPageBody(path: location), + child: _Body(path: location), ), ); } } + +class _Body extends HookConsumerWidget { + const _Body({ + required this.path, + }); + + final String path; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = Translations.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.notFoundPage.header.label), + const Gap(32), + DisplaySmallText(path), + const Gap(32), + Text(t.notFoundPage.description), + Center( + child: ElevatedButton( + onPressed: () => const HomeRouteData().go(context), + child: Text(t.notFoundPage.backButton.label), + ), + ), + ], + ); + } +} diff --git a/packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_app_bar.dart b/packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_app_bar.dart deleted file mode 100644 index f64983cf..00000000 --- a/packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_app_bar.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:themes/themes.dart'; - -import '../../../gen/strings.g.dart'; - -class PinkieMewPageAppBar extends StatelessWidget - implements PreferredSizeWidget { - const PinkieMewPageAppBar({super.key}); - - @override - Size get preferredSize => appBarDefaultSize; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return AppBar( - title: Text(t.pinkieMewPage.title), - ); - } -} diff --git a/packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_body.dart b/packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_body.dart deleted file mode 100644 index 2c88d74c..00000000 --- a/packages/flutter_app/lib/pages/pinkie_mew/components/pinkie_mew_page_body.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class PinkieMewPageBody extends HookConsumerWidget { - const PinkieMewPageBody({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Container(); - } -} diff --git a/packages/flutter_app/lib/pages/pinkie_mew/pinkie_mew_page.dart b/packages/flutter_app/lib/pages/pinkie_mew/pinkie_mew_page.dart deleted file mode 100644 index c59ace16..00000000 --- a/packages/flutter_app/lib/pages/pinkie_mew/pinkie_mew_page.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import 'components/pinkie_mew_page_app_bar.dart'; -import 'components/pinkie_mew_page_body.dart'; - -class PinkieMewPage extends HookConsumerWidget { - const PinkieMewPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return const Scaffold( - appBar: PinkieMewPageAppBar(), - body: PinkieMewPageBody(), - ); - } -} diff --git a/packages/flutter_app/lib/pages/settings_page/components/settings_page_app_bar.dart b/packages/flutter_app/lib/pages/settings_page/components/settings_page_app_bar.dart deleted file mode 100644 index dab50082..00000000 --- a/packages/flutter_app/lib/pages/settings_page/components/settings_page_app_bar.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:themes/themes.dart'; - -import '../../../gen/strings.g.dart'; - -class SettingsPageAppBar extends StatelessWidget - implements PreferredSizeWidget { - const SettingsPageAppBar({super.key}); - - @override - Size get preferredSize => appBarDefaultSize; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - return AppBar( - title: Text(t.settingsPage.appBar.title), - ); - } -} diff --git a/packages/flutter_app/lib/pages/settings_page/components/settings_page_body.dart b/packages/flutter_app/lib/pages/settings_page/components/settings_page_body.dart deleted file mode 100644 index 5c89bc47..00000000 --- a/packages/flutter_app/lib/pages/settings_page/components/settings_page_body.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../../features/user_device/user_device.dart'; -import '../../../gen/strings.g.dart'; -import '../../../router/router.dart'; - -class SettingsPageBody extends HookConsumerWidget { - const SettingsPageBody({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = Translations.of(context); - final userDevice = ref.watch(userDeviceProvider); - return ListView( - padding: const EdgeInsets.all(16), - children: [ - Card( - child: ListTile( - onTap: () => const ThemeSelectionRouteData().go(context), - title: Text(t.settingsPage.list.themeSelector.label), - ), - ), - Card( - child: ListTile( - onTap: () => const AccountRouteData().go(context), - title: Text(t.settingsPage.list.account.label), - ), - ), - ListTile( - title: Text(userDevice.name), - subtitle: - Text('${userDevice.model}, OS: ${userDevice.osVersionString}'), - ), - ], - ); - } -} diff --git a/packages/flutter_app/lib/pages/settings_page/settings_page.dart b/packages/flutter_app/lib/pages/settings_page/settings_page.dart index 0b830f06..16a3f1cc 100644 --- a/packages/flutter_app/lib/pages/settings_page/settings_page.dart +++ b/packages/flutter_app/lib/pages/settings_page/settings_page.dart @@ -1,17 +1,55 @@ import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'components/settings_page_body.dart'; +import '../../features/user_device/user_device.dart'; +import '../../gen/strings.g.dart'; +import '../../router/router.dart'; +/// A page for settings. class SettingsPage extends StatelessWidget { + /// Creates a new instance of [SettingsPage]. const SettingsPage({super.key}); @override Widget build(BuildContext context) { + final t = Translations.of(context); return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text(t.settingsPage.appBar.title), ), - body: const SettingsPageBody(), + body: const _Body(), + ); + } +} + +class _Body extends HookConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = Translations.of(context); + final userDevice = ref.watch(userDeviceProvider); + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Card( + child: ListTile( + onTap: () => const ThemeSelectionRouteData().go(context), + title: Text(t.settingsPage.list.themeSelector.label), + ), + ), + Card( + child: ListTile( + onTap: () => const AccountRouteData().go(context), + title: Text(t.settingsPage.list.account.label), + ), + ), + ListTile( + title: Text(userDevice.name), + subtitle: + Text('${userDevice.model}, OS: ${userDevice.osVersionString}'), + ), + ], ); } } diff --git a/packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_app_bar.dart b/packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_app_bar.dart deleted file mode 100644 index d7aca49a..00000000 --- a/packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_app_bar.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:themes/themes.dart'; - -import '../../../gen/strings.g.dart'; -import '../../../widgets/widgets.dart'; - -class ThemeSelectionPageAppBar extends StatelessWidget - implements PreferredSizeWidget { - const ThemeSelectionPageAppBar({super.key}); - - @override - Size get preferredSize => appBarDefaultSize; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - final themeDescription = context.themeDescription; - return AppBar( - title: Text(t.theme.selection.page.appBar.title), - actions: [ - themeDescription.icon, - Align( - child: TitleLargeText(themeDescription.title), - ), - const Gap(16), - ], - ); - } -} diff --git a/packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_body.dart b/packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_body.dart deleted file mode 100644 index 384ed60f..00000000 --- a/packages/flutter_app/lib/pages/theme_selection_page/components/theme_selection_page_body.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../../features/theme_selector/theme_selector.dart'; -import '../../../gen/strings.g.dart'; - -class ThemeSelectionPageBody extends HookConsumerWidget { - const ThemeSelectionPageBody({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final themeSelector = ref.watch(themeSelectorProvider.notifier); - final currentThemeMode = ref.watch(themeSelectorProvider); - final t = Translations.of(context); - return ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 16), - itemCount: ThemeMode.values.length, - itemBuilder: (_, index) { - final themeMode = ThemeMode.values[index]; - return RadioListTile( - value: themeMode, - groupValue: currentThemeMode, - onChanged: (newTheme) async => themeSelector.changeAndSave(newTheme!), - title: Text(themeMode.title(t)), - subtitle: Text(themeMode.subtitle(t)), - secondary: Icon(themeMode.iconData), - ); - }, - ); - } -} diff --git a/packages/flutter_app/lib/pages/theme_selection_page/theme_selection_page.dart b/packages/flutter_app/lib/pages/theme_selection_page/theme_selection_page.dart index ca328017..0d9935f2 100644 --- a/packages/flutter_app/lib/pages/theme_selection_page/theme_selection_page.dart +++ b/packages/flutter_app/lib/pages/theme_selection_page/theme_selection_page.dart @@ -1,16 +1,59 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:themes/themes.dart'; -import 'components/theme_selection_page_app_bar.dart'; -import 'components/theme_selection_page_body.dart'; +import '../../features/theme_selector/theme_selector.dart'; +import '../../gen/strings.g.dart'; +import '../../widgets/widgets.dart'; -class ThemeSelectionPage extends StatelessWidget { +/// Page for selecting a theme. +class ThemeSelectionPage extends ConsumerWidget { + /// Creates a new instance of [ThemeSelectionPage]. const ThemeSelectionPage({super.key}); @override - Widget build(BuildContext context) { - return const Scaffold( - appBar: ThemeSelectionPageAppBar(), - body: ThemeSelectionPageBody(), + Widget build(BuildContext context, WidgetRef ref) { + final t = Translations.of(context); + final themeDescription = context.themeDescription; + return Scaffold( + appBar: AppBar( + title: Text(t.theme.selection.page.appBar.title), + actions: [ + themeDescription.icon, + Align( + child: TitleLargeText(themeDescription.title), + ), + const Gap(16), + ], + ), + body: const _Body(), + ); + } +} + +class _Body extends HookConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeSelector = ref.watch(themeSelectorProvider.notifier); + final currentThemeMode = ref.watch(themeSelectorProvider); + final t = Translations.of(context); + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: ThemeMode.values.length, + itemBuilder: (_, index) { + final themeMode = ThemeMode.values[index]; + return RadioListTile( + value: themeMode, + groupValue: currentThemeMode, + onChanged: (newTheme) async => themeSelector.changeAndSave(newTheme!), + title: Text(themeMode.title(t)), + subtitle: Text(themeMode.subtitle(t)), + secondary: Icon(themeMode.iconData), + ); + }, ); } } diff --git a/packages/themes/lib/src/altive_theme/altive_theme.dart b/packages/themes/lib/src/altive_theme/altive_theme.dart index 2e33c6ee..b09d756f 100644 --- a/packages/themes/lib/src/altive_theme/altive_theme.dart +++ b/packages/themes/lib/src/altive_theme/altive_theme.dart @@ -1,5 +1,4 @@ export 'dark_theme_data.dart'; -export 'layout.dart'; export 'light_theme_data.dart'; export 'theme_extensions/theme_extensions.dart'; export 'utils/build_context_extension.dart'; diff --git a/packages/themes/lib/src/altive_theme/layout.dart b/packages/themes/lib/src/altive_theme/layout.dart deleted file mode 100644 index 3544ce7c..00000000 --- a/packages/themes/lib/src/altive_theme/layout.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:flutter/material.dart'; - -const appBarDefaultSize = Size(double.infinity, kToolbarHeight);