diff --git a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart new file mode 100644 index 0000000000..b45666ac4a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart @@ -0,0 +1,125 @@ +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/application/document_listener.dart'; +import 'package:appflowy/plugins/document/application/document_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'recent_view_bloc.freezed.dart'; + +class RecentViewBloc extends Bloc { + RecentViewBloc({ + required this.view, + }) : _documentListener = DocumentListener(id: view.id), + _viewListener = ViewListener(viewId: view.id), + super(RecentViewState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + _documentListener.start( + onDocEventUpdate: (docEvent) async { + final (coverType, coverValue) = await getCover(); + add( + RecentViewEvent.updateCover( + coverType, + coverValue, + ), + ); + }, + ); + _viewListener.start( + onViewUpdated: (view) { + add( + RecentViewEvent.updateNameOrIcon( + view.name, + view.icon.value, + ), + ); + }, + ); + final (coverType, coverValue) = await getCover(); + emit( + state.copyWith( + name: view.name, + icon: view.icon.value, + coverType: coverType, + coverValue: coverValue, + ), + ); + }, + updateNameOrIcon: (name, icon) { + emit( + state.copyWith( + name: name, + icon: icon, + ), + ); + }, + updateCover: (coverType, coverValue) { + emit( + state.copyWith( + coverType: coverType, + coverValue: coverValue, + ), + ); + }, + ); + }, + ); + } + + final _service = DocumentService(); + final ViewPB view; + final DocumentListener _documentListener; + final ViewListener _viewListener; + + Future<(CoverType, String?)> getCover() async { + final result = await _service.getDocument(viewId: view.id); + final document = result.fold((s) => s.toDocument(), (f) => null); + if (document != null) { + final coverType = CoverType.fromString( + document.root.attributes[DocumentHeaderBlockKeys.coverType], + ); + final coverValue = document + .root.attributes[DocumentHeaderBlockKeys.coverDetails] as String?; + return (coverType, coverValue); + } + return (CoverType.none, null); + } + + @override + Future close() async { + await _documentListener.stop(); + await _viewListener.stop(); + return super.close(); + } +} + +@freezed +class RecentViewEvent with _$RecentViewEvent { + const factory RecentViewEvent.initial() = Initial; + const factory RecentViewEvent.updateCover( + CoverType coverType, + String? coverValue, + ) = UpdateCover; + const factory RecentViewEvent.updateNameOrIcon( + String name, + String icon, + ) = UpdateNameOrIcon; +} + +@freezed +class RecentViewState with _$RecentViewState { + const factory RecentViewState({ + required String name, + required String icon, + @Default(CoverType.none) CoverType coverType, + @Default(null) String? coverValue, + }) = _RecentViewState; + + factory RecentViewState.initial() => + const RecentViewState(name: '', icon: ''); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index 1cf3b5515b..3d80adc3e6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -117,49 +117,21 @@ class _RecentViews extends StatelessWidget { }, ), ), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 24), - // child: FlowyText.semibold( - // LocaleKeys.sideBar_recent.tr(), - // fontSize: 20.0, - // ), - // ), - // if (kDebugMode) - // Padding( - // padding: const EdgeInsets.only(right: 16.0), - // child: FlowyButton( - // useIntrinsicWidth: true, - // text: FlowyText(LocaleKeys.button_clear.tr()), - // onTap: () { - // context.read().add( - // RecentViewsEvent.removeRecentViews( - // recentViews.map((e) => e.id).toList(), - // ), - // ); - // }, - // ), - // ), - // ], - // ), - SingleChildScrollView( - key: const PageStorageKey('recent_views_page_storage_key'), - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: IntrinsicHeight( - child: SeparatedRow( - separatorBuilder: () => const HSpace(8), - children: recentViews - .map( - (view) => SizedBox.square( - dimension: 148, - child: MobileRecentView(view: view), - ), - ) - .toList(), - ), + SizedBox( + height: 148, + child: ListView.separated( + key: const PageStorageKey('recent_views_page_storage_key'), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final view = recentViews[index]; + return SizedBox.square( + dimension: 148, + child: MobileRecentView(view: view), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: recentViews.length, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart index 41d6bf3f5b..cc3258ad5d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -1,24 +1,21 @@ import 'dart:io'; import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; -import 'package:appflowy/plugins/document/application/document_listener.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:string_validator/string_validator.dart'; -class MobileRecentView extends StatefulWidget { +class MobileRecentView extends StatelessWidget { const MobileRecentView({ super.key, required this.view, @@ -26,181 +23,136 @@ class MobileRecentView extends StatefulWidget { final ViewPB view; - @override - State createState() => _MobileRecentViewState(); -} - -class _MobileRecentViewState extends State { - late final ViewListener viewListener; - late ViewPB view; - late final DocumentListener documentListener; - - @override - void initState() { - super.initState(); - - view = widget.view; - - viewListener = ViewListener( - viewId: view.id, - )..start( - onViewUpdated: (view) { - setState(() { - this.view = view; - }); - }, - ); - - documentListener = DocumentListener(id: view.id) - ..start( - onDocEventUpdate: (document) { - setState(() { - view = view; - }); - }, - ); - } - - @override - void dispose() { - viewListener.stop(); - documentListener.stop(); - - super.dispose(); - } - @override Widget build(BuildContext context) { - final icon = view.icon.value; final theme = Theme.of(context); - return GestureDetector( - onTap: () => context.pushView(view), - child: Stack( - children: [ - DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: theme.colorScheme.outline), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + return BlocProvider( + create: (context) => RecentViewBloc(view: view) + ..add( + const RecentViewEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return GestureDetector( + onTap: () => context.pushView(view), + child: Stack( children: [ - Expanded( - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - child: _buildCoverWidget(), + DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: theme.colorScheme.outline), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + child: _RecentCover( + coverType: state.coverType, + value: state.coverValue, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 18, 8, 2), + // hack: minLines currently not supported in Text widget. + // https://github.com/flutter/flutter/issues/31134 + child: Stack( + children: [ + FlowyText.medium( + view.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const FlowyText( + "\n\n", + maxLines: 2, + ), + ], + ), + ), + ), + ], ), ), - Expanded( + Align( + alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.fromLTRB(8, 18, 8, 2), - // hack: minLines currently not supported in Text widget. - // https://github.com/flutter/flutter/issues/31134 - child: Stack( - children: [ - FlowyText.medium( - view.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const FlowyText( - "\n\n", - maxLines: 2, - ), - ], - ), + padding: const EdgeInsets.only(left: 8.0), + child: state.icon.isNotEmpty + ? EmojiText( + emoji: state.icon, + fontSize: 30.0, + ) + : SizedBox.square( + dimension: 32.0, + child: view.defaultIcon(), + ), ), ), ], ), - ), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: icon.isNotEmpty - ? EmojiText( - emoji: icon, - fontSize: 30.0, - ) - : SizedBox.square( - dimension: 32.0, - child: view.defaultIcon(), - ), - ), - ), - ], + ); + }, ), ); } +} - Widget _buildCoverWidget() { - return FutureBuilder( - future: _getPageNode(), - builder: (context, snapshot) { - final node = snapshot.data; - final placeholder = Container( - // random color, update it once we have a better placeholder - color: - Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2), - ); - if (node == null) { - return placeholder; - } - final type = CoverType.fromString( - node.attributes[DocumentHeaderBlockKeys.coverType], - ); - final cover = - node.attributes[DocumentHeaderBlockKeys.coverDetails] as String?; - if (cover == null) { - return placeholder; - } - switch (type) { - case CoverType.file: - if (isURL(cover)) { - final userProfilePB = Provider.of(context); - return FlowyNetworkImage( - url: cover, - userProfilePB: userProfilePB, - ); - } - final imageFile = File(cover); - if (!imageFile.existsSync()) { - return placeholder; - } - return Image.file( - imageFile, - ); - case CoverType.asset: - return Image.asset( - cover, - fit: BoxFit.cover, - ); - case CoverType.color: - final color = cover.tryToColor() ?? Colors.white; - return Container( - color: color, - ); - case CoverType.none: - return placeholder; - } - }, +class _RecentCover extends StatelessWidget { + const _RecentCover({ + required this.coverType, + this.value, + }); + + final CoverType coverType; + final String? value; + + @override + Widget build(BuildContext context) { + final placeholder = Container( + // random color, update it once we have a better placeholder + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2), ); - } - - Future _getPageNode() async { - final data = await DocumentEventGetDocumentData( - OpenDocumentPayloadPB(documentId: view.id), - ).send(); - final document = data.fold((l) => l.toDocument(), (r) => null); - if (document != null) { - return document.root; + final value = this.value; + if (value == null) { + return placeholder; + } + switch (coverType) { + case CoverType.file: + if (isURL(value)) { + final userProfilePB = Provider.of(context); + return FlowyNetworkImage( + url: value, + userProfilePB: userProfilePB, + ); + } + final imageFile = File(value); + if (!imageFile.existsSync()) { + return placeholder; + } + return Image.file( + imageFile, + ); + case CoverType.asset: + return Image.asset( + value, + fit: BoxFit.cover, + ); + case CoverType.color: + final color = value.tryToColor() ?? Colors.white; + return Container( + color: color, + ); + case CoverType.none: + return placeholder; } - return null; } }