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(
// 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<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(),
),
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,
),
),
],

View File

@ -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<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
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<RecentViewBloc>(
create: (context) => RecentViewBloc(view: view)
..add(
const RecentViewEvent.initial(),
),
child: BlocBuilder<RecentViewBloc, RecentViewState>(
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<Node?>(
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<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;
}
},
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<Node?> _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<UserProfilePB?>(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;
}
}