fix: retry count should be clear if the value exceeds max retries (#7195)

* fix: retry count should be clear if the value exceeds max retries

* feat: add retry button when loading image failed
This commit is contained in:
Lucas 2025-01-13 17:17:05 +08:00 committed by GitHub
parent 0c6d4df14b
commit fc21d1d245
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 23 deletions

View File

@ -255,14 +255,17 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
final url = node.attributes[CustomImageBlockKeys.url];
return Stack(
children: [
BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor: editorState.editorStyle.selectionColor,
child: child!,
),
editorState.editable
? BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor:
editorState.editorStyle.selectionColor,
child: child!,
)
: child!,
if (value && url.isNotEmpty == true)
widget.menuBuilder!(widget.node, this, imageStateNotifier),
],

View File

@ -2,12 +2,14 @@ import 'dart:io';
import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -125,6 +127,12 @@ class _ResizableImageState extends State<ResizableImage> {
return _ImageLoadFailedWidget(
width: imageWidth,
error: error,
onRetry: () {
setState(() {
final retryCounter = FlowyNetworkRetryCounter();
retryCounter.clear(tag: src, url: src);
});
},
);
},
);
@ -236,19 +244,24 @@ class _ResizableImageState extends State<ResizableImage> {
}
class _ImageLoadFailedWidget extends StatelessWidget {
const _ImageLoadFailedWidget({required this.width, required this.error});
const _ImageLoadFailedWidget({
required this.width,
required this.error,
required this.onRetry,
});
final double width;
final Object error;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
final error = _getErrorMessage();
return Container(
height: 140,
height: 160,
width: width,
alignment: Alignment.center,
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
border: Border.all(color: Colors.grey.withOpacity(0.6)),
@ -258,10 +271,13 @@ class _ImageLoadFailedWidget extends StatelessWidget {
children: [
const FlowySvg(
FlowySvgs.broken_image_xl,
size: Size.square(48),
size: Size.square(36),
),
FlowyText(AppFlowyEditorL10n.current.imageLoadFailed),
const VSpace(6),
FlowyText(
AppFlowyEditorL10n.current.imageLoadFailed,
fontSize: 14,
),
const VSpace(4),
if (error != null)
FlowyText(
error,
@ -270,6 +286,11 @@ class _ImageLoadFailedWidget extends StatelessWidget {
fontSize: 10,
maxLines: 2,
),
const VSpace(12),
OutlinedRoundedButton(
text: LocaleKeys.chat_retry.tr(),
onTap: onRetry,
),
],
),
);

View File

@ -71,7 +71,7 @@ class FlowyNetworkImage extends StatefulWidget {
class FlowyNetworkImageState extends State<FlowyNetworkImage> {
final manager = CustomImageCacheManager();
final retryCounter = _FlowyNetworkRetryCounter();
final retryCounter = FlowyNetworkRetryCounter();
// This is used to clear the retry count when the widget is disposed in case of the url is the same.
String? retryTag;
@ -104,14 +104,22 @@ class FlowyNetworkImageState extends State<FlowyNetworkImage> {
super.reassemble();
if (retryTag != null) {
retryCounter.clear(retryTag!);
retryCounter.clear(
tag: retryTag!,
url: widget.url,
maxRetries: widget.maxRetries,
);
}
}
@override
void dispose() {
if (retryTag != null) {
retryCounter.clear(retryTag!);
retryCounter.clear(
tag: retryTag!,
url: widget.url,
maxRetries: widget.maxRetries,
);
}
super.dispose();
@ -204,11 +212,12 @@ class FlowyNetworkImageState extends State<FlowyNetworkImage> {
}
/// This class is used to count the number of retries for a given URL.
class _FlowyNetworkRetryCounter with ChangeNotifier {
_FlowyNetworkRetryCounter._();
@visibleForTesting
class FlowyNetworkRetryCounter with ChangeNotifier {
FlowyNetworkRetryCounter._();
factory _FlowyNetworkRetryCounter() => _instance;
static final _instance = _FlowyNetworkRetryCounter._();
factory FlowyNetworkRetryCounter() => _instance;
static final _instance = FlowyNetworkRetryCounter._();
final Map<String, int> _values = <String, int>{};
Map<String, int> get values => {..._values};
@ -236,9 +245,19 @@ class _FlowyNetworkRetryCounter with ChangeNotifier {
notifyListeners();
}
/// Clear the retry count for a given URL.
void clear(String tag) {
/// Clear the retry count for a given tag.
void clear({
required String tag,
required String url,
int? maxRetries,
}) {
_values.remove(tag);
final retryCount = _values[url];
if (maxRetries == null ||
(retryCount != null && retryCount >= maxRetries)) {
_values.remove(url);
}
}
/// Reset the retry counter.

View File

@ -0,0 +1,35 @@
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AppFlowy Network Image:', () {
setUpAll(() {
Log.shared.disableLog = true;
});
tearDownAll(() {
Log.shared.disableLog = false;
});
test(
'retry count should be clear if the value exceeds max retries',
() async {
const maxRetries = 5;
const fakeUrl = 'https://plus.unsplash.com/premium_photo-1731948132439';
final retryCounter = FlowyNetworkRetryCounter();
final tag = retryCounter.add(fakeUrl);
for (var i = 0; i < maxRetries; i++) {
retryCounter.increment(fakeUrl);
expect(retryCounter.getRetryCount(fakeUrl), i + 1);
}
retryCounter.clear(
tag: tag,
url: fakeUrl,
maxRetries: maxRetries,
);
expect(retryCounter.getRetryCount(fakeUrl), 0);
},
);
});
}