fix: only fetch visible recent view (#5090)

* fix: only fetch visible recent view

* fix: flutter analyze
This commit is contained in:
Lucas.Xu 2024-04-09 20:01:29 +08:00 committed by GitHub
parent 72049d28d5
commit 8042be6575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 256 additions and 207 deletions

View File

@ -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<RecentViewEvent, RecentViewState> {
RecentViewBloc({
required this.view,
}) : _documentListener = DocumentListener(id: view.id),
_viewListener = ViewListener(viewId: view.id),
super(RecentViewState.initial()) {
on<RecentViewEvent>(
(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<void> 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: '');
}

View File

@ -117,49 +117,21 @@ class _RecentViews extends StatelessWidget {
}, },
), ),
), ),
// Row( SizedBox(
// mainAxisAlignment: MainAxisAlignment.spaceBetween, height: 148,
// children: [ child: ListView.separated(
// Padding( key: const PageStorageKey('recent_views_page_storage_key'),
// padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
// child: FlowyText.semibold( scrollDirection: Axis.horizontal,
// LocaleKeys.sideBar_recent.tr(), itemBuilder: (context, index) {
// fontSize: 20.0, final view = recentViews[index];
// ), return SizedBox.square(
// ), dimension: 148,
// if (kDebugMode) child: MobileRecentView(view: view),
// Padding( );
// padding: const EdgeInsets.only(right: 16.0), },
// child: FlowyButton( separatorBuilder: (context, index) => const HSpace(8),
// useIntrinsicWidth: true, itemCount: recentViews.length,
// text: FlowyText(LocaleKeys.button_clear.tr()),
// onTap: () {
// context.read<RecentViewsBloc>().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(),
),
), ),
), ),
], ],

View File

@ -1,24 +1,21 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart'; 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/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/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.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/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-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart'; import 'package:string_validator/string_validator.dart';
class MobileRecentView extends StatefulWidget { class MobileRecentView extends StatelessWidget {
const MobileRecentView({ const MobileRecentView({
super.key, super.key,
required this.view, required this.view,
@ -26,181 +23,136 @@ class MobileRecentView extends StatefulWidget {
final ViewPB view; final ViewPB view;
@override
State<MobileRecentView> createState() => _MobileRecentViewState();
}
class _MobileRecentViewState extends State<MobileRecentView> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final icon = view.icon.value;
final theme = Theme.of(context); final theme = Theme.of(context);
return GestureDetector( return BlocProvider<RecentViewBloc>(
onTap: () => context.pushView(view), create: (context) => RecentViewBloc(view: view)
child: Stack( ..add(
children: [ const RecentViewEvent.initial(),
DecoratedBox( ),
decoration: BoxDecoration( child: BlocBuilder<RecentViewBloc, RecentViewState>(
borderRadius: BorderRadius.circular(8), builder: (context, state) {
border: Border.all(color: theme.colorScheme.outline), return GestureDetector(
), onTap: () => context.pushView(view),
child: Column( child: Stack(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Expanded( DecoratedBox(
child: ClipRRect( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: BorderRadius.circular(8),
topLeft: Radius.circular(8), border: Border.all(color: theme.colorScheme.outline),
topRight: Radius.circular(8), ),
), child: Column(
child: _buildCoverWidget(), 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( child: Padding(
padding: const EdgeInsets.fromLTRB(8, 18, 8, 2), padding: const EdgeInsets.only(left: 8.0),
// hack: minLines currently not supported in Text widget. child: state.icon.isNotEmpty
// https://github.com/flutter/flutter/issues/31134 ? EmojiText(
child: Stack( emoji: state.icon,
children: [ fontSize: 30.0,
FlowyText.medium( )
view.name, : SizedBox.square(
maxLines: 2, dimension: 32.0,
overflow: TextOverflow.ellipsis, child: view.defaultIcon(),
), ),
const FlowyText(
"\n\n",
maxLines: 2,
),
],
),
), ),
), ),
], ],
), ),
), );
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() { class _RecentCover extends StatelessWidget {
return FutureBuilder<Node?>( const _RecentCover({
future: _getPageNode(), required this.coverType,
builder: (context, snapshot) { this.value,
final node = snapshot.data; });
final placeholder = Container(
// random color, update it once we have a better placeholder final CoverType coverType;
color: final String? value;
Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2),
); @override
if (node == null) { Widget build(BuildContext context) {
return placeholder; final placeholder = Container(
} // random color, update it once we have a better placeholder
final type = CoverType.fromString( color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2),
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<UserProfilePB?>(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;
}
},
); );
} final value = this.value;
if (value == null) {
Future<Node?> _getPageNode() async { return placeholder;
final data = await DocumentEventGetDocumentData( }
OpenDocumentPayloadPB(documentId: view.id), switch (coverType) {
).send(); case CoverType.file:
final document = data.fold((l) => l.toDocument(), (r) => null); if (isURL(value)) {
if (document != null) { final userProfilePB = Provider.of<UserProfilePB?>(context);
return document.root; 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;
} }
} }