Skip to content

Commit

Permalink
Merge pull request #1540 from nextcloud/refactor/neon_framework/image…
Browse files Browse the repository at this point in the history
…-caching-request-manager
  • Loading branch information
provokateurin authored Jan 31, 2024
2 parents 3147216 + 4f86ec9 commit 79d20d0
Show file tree
Hide file tree
Showing 19 changed files with 524 additions and 300 deletions.
8 changes: 0 additions & 8 deletions packages/app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
flutter_driver:
dependency: transitive
description: flutter
Expand Down
2 changes: 1 addition & 1 deletion packages/neon/neon_dashboard/lib/src/pages/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ class DashboardMainPage extends StatelessWidget {
final colorFilter = ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn);

if (widget.iconUrl.isNotEmpty) {
return NeonUrlImage(
return NeonUriImage(
uri: Uri.parse(widget.iconUrl),
svgColorFilter: colorFilter,
size: const Size.square(largeIconSize),
Expand Down
4 changes: 2 additions & 2 deletions packages/neon/neon_dashboard/lib/src/widgets/widget_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class DashboardWidgetItem extends StatelessWidget {
child: NeonImageWrapper(
borderRadius: roundIcon ? BorderRadius.circular(largeIconSize) : null,
child: item.iconUrl.isNotEmpty
? NeonUrlImage(
? NeonUriImage(
uri: Uri.parse(item.iconUrl),
size: const Size.square(largeIconSize),
)
Expand All @@ -48,7 +48,7 @@ class DashboardWidgetItem extends StatelessWidget {
alignment: Alignment.bottomRight,
child: SizedBox.square(
dimension: smallIconSize,
child: NeonUrlImage(
child: NeonUriImage(
uri: Uri.parse(overlayIconUrl),
size: const Size.square(smallIconSize),
),
Expand Down
8 changes: 3 additions & 5 deletions packages/neon/neon_dashboard/test/widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ Widget wrapWidget(AccountsBloc accountsBloc, Widget child) => MaterialApp(
);

void main() {
NeonCachedImage.cacheManager = MockCacheManager();

final accountsBloc = MockAccountsBloc();
when(() => accountsBloc.activeAccount).thenAnswer(
(invocation) => BehaviorSubject.seeded(
Expand Down Expand Up @@ -84,7 +82,7 @@ void main() {
BorderRadius.circular(largeIconSize),
),
);
expect(find.byType(NeonCachedImage), findsNWidgets(2));
expect(find.byType(NeonImage), findsNWidgets(2));

await expectLater(find.byType(DashboardWidgetItem), matchesGoldenFile('goldens/widget_item.png'));
});
Expand Down Expand Up @@ -144,7 +142,7 @@ void main() {
),
);

expect(find.byType(NeonCachedImage), findsOneWidget);
expect(find.byType(NeonImage), findsOneWidget);
});
});

Expand Down Expand Up @@ -293,7 +291,7 @@ void main() {
BorderRadius.circular(largeIconSize),
),
);
expect(find.byType(NeonCachedImage), findsNWidgets(3));
expect(find.byType(NeonImage), findsNWidgets(3));
expect(find.byType(DashboardWidgetItem), findsOneWidget);
expect(find.bySubtype<FilledButton>(), findsOneWidget);
expect(find.byIcon(Icons.add), findsOneWidget);
Expand Down
12 changes: 3 additions & 9 deletions packages/neon/neon_files/lib/src/widgets/file_preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,13 @@ class FilePreviewImage extends NeonApiImage {
required int width,
required int height,
}) : super(
getImage: (client) async => client.core.preview.getPreview(
getImage: (client) => client.core.preview.getPreviewRaw(
file: file.uri.path,
x: width,
y: height,
),
writeCache: (cacheManager, data) async {
await cacheManager.putFile(
cacheKey,
data,
maxAge: const Duration(days: 7),
eTag: file.etag,
);
},
etag: file.etag,
expires: null,
isSvgHint: file.mimeType?.contains('svg') ?? false,
);
}
2 changes: 1 addition & 1 deletion packages/neon/neon_news/lib/src/widgets/articles_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
),
),
if (article.mediaThumbnail != null) ...[
NeonUrlImage(
NeonUriImage(
uri: Uri.parse(article.mediaThumbnail!),
size: const Size(100, 50),
fit: BoxFit.cover,
Expand Down
2 changes: 1 addition & 1 deletion packages/neon/neon_news/lib/src/widgets/feed_icon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class NewsFeedIcon extends StatelessWidget {
size: Size.square(size),
borderRadius: borderRadius,
child: faviconLink != null && faviconLink.isNotEmpty
? NeonUrlImage(
? NeonUriImage(
uri: Uri.parse(faviconLink),
size: Size.square(size),
)
Expand Down
2 changes: 1 addition & 1 deletion packages/neon/neon_notifications/lib/src/pages/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
)
: SizedBox.fromSize(
size: const Size.square(largeIconSize),
child: NeonUrlImage(
child: NeonUriImage(
uri: Uri.parse(notification.icon!),
size: const Size.square(largeIconSize),
svgColorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
Expand Down
3 changes: 0 additions & 3 deletions packages/neon_framework/lib/src/testing/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import 'dart:async';

import 'package:flutter_cache_manager/flutter_cache_manager.dart';
// ignore: depend_on_referenced_packages
import 'package:mocktail/mocktail.dart';
import 'package:neon_framework/blocs.dart';
Expand Down Expand Up @@ -51,5 +50,3 @@ class MockSharedPreferences extends Mock implements SharedPreferences {}
class MockCallbackFunction<T> extends Mock {
FutureOr<T> call();
}

class MockCacheManager extends Mock implements DefaultCacheManager {}
65 changes: 41 additions & 24 deletions packages/neon_framework/lib/src/utils/push_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ import 'dart:ui';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_svg/flutter_svg.dart' show vg;
import 'package:flutter_svg/flutter_svg.dart' show SvgBytesLoader, vg;
import 'package:image/image.dart' as img;
import 'package:meta/meta.dart';
import 'package:neon_framework/src/bloc/result.dart';
import 'package:neon_framework/src/blocs/accounts.dart';
import 'package:neon_framework/src/models/account.dart';
import 'package:neon_framework/src/models/push_notification.dart';
import 'package:neon_framework/src/settings/models/storage.dart';
import 'package:neon_framework/src/theme/colors.dart';
import 'package:neon_framework/src/utils/findable.dart';
import 'package:neon_framework/src/utils/image_utils.dart';
import 'package:neon_framework/src/utils/localizations.dart';
import 'package:neon_framework/src/utils/universal_svg_file_loader.dart';
import 'package:neon_framework/src/utils/request_manager.dart';
import 'package:nextcloud/notifications.dart' as notifications;
import 'package:rxdart/rxdart.dart';

@internal
@immutable
Expand Down Expand Up @@ -118,27 +120,42 @@ class PushUtils {
if (notification.icon?.endsWith('.svg') ?? false) {
// Only SVG icons are supported right now (should be most of them)

final cacheManager = DefaultCacheManager();
final file = await cacheManager.getSingleFile(notification.icon!);

final pictureInfo = await vg.loadPicture(UniversalSvgFileLoader(file), null);

const largeIconSize = 256;
final scale = largeIconSize / pictureInfo.size.longestSide;
final scaledSize = pictureInfo.size * scale;

final recorder = PictureRecorder();
Canvas(recorder)
..scale(scale)
..drawPicture(pictureInfo.picture)
..drawColor(NcColors.primary, BlendMode.srcIn);

pictureInfo.picture.dispose();

final image = recorder.endRecording().toImageSync(scaledSize.width.toInt(), scaledSize.height.toInt());
final bytes = await image.toByteData(format: ImageByteFormat.png);

largeIconBitmap = ByteArrayAndroidBitmap(img.encodeBmp(img.decodePng(bytes!.buffer.asUint8List())!));
final uri = Uri.parse(notification.icon!);
final subject = BehaviorSubject<Result<Uint8List>>();
await RequestManager.instance.wrapUri(
account: account,
uri: uri,
unwrap: (data) {
try {
return utf8.encode(ImageUtils.rewriteSvgDimensions(utf8.decode(data)));
} catch (_) {}
return data;
},
subject: subject,
);
final rawImage = subject.valueOrNull?.data;
unawaited(subject.close());

if (rawImage != null) {
final pictureInfo = await vg.loadPicture(SvgBytesLoader(rawImage), null);

const largeIconSize = 256;
final scale = largeIconSize / pictureInfo.size.longestSide;
final scaledSize = pictureInfo.size * scale;

final recorder = PictureRecorder();
Canvas(recorder)
..scale(scale)
..drawPicture(pictureInfo.picture)
..drawColor(NcColors.primary, BlendMode.srcIn);

pictureInfo.picture.dispose();

final image = recorder.endRecording().toImageSync(scaledSize.width.toInt(), scaledSize.height.toInt());
final bytes = await image.toByteData(format: ImageByteFormat.png);

largeIconBitmap = ByteArrayAndroidBitmap(img.encodeBmp(img.decodePng(bytes!.buffer.asUint8List())!));
}
}
}
} catch (e, s) {
Expand Down
79 changes: 79 additions & 0 deletions packages/neon_framework/lib/src/utils/request_manager.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';

import 'package:built_value/serializer.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
Expand Down Expand Up @@ -131,6 +132,84 @@ class RequestManager {
disableTimeout: disableTimeout,
);

/// Executes a HTTP request for binary content.
Future<void> wrapBinary({
required Account account,
required String cacheKey,
required AsyncValueGetter<CacheParameters> getCacheParameters,
required DynamiteRawResponse<Uint8List, dynamic> rawResponse,
required UnwrapCallback<Uint8List, Uint8List>? unwrap,
required BehaviorSubject<Result<Uint8List>> subject,
bool disableTimeout = false,
}) async =>
wrap<Uint8List, DynamiteRawResponse<Uint8List, dynamic>>(
account: account,
cacheKey: cacheKey,
subject: subject,
request: () async {
await rawResponse.future;
return rawResponse;
},
unwrap: (rawResponse) {
var data = rawResponse.response.body;
if (unwrap != null) {
data = unwrap(data);
}

return data;
},
serialize: (rawResponse) => base64.encode(rawResponse.response.body),
deserialize: (data) => DynamiteRawResponse<Uint8List, Map<String, String>>.fromJson(
{
'statusCode': 200,
'body': base64.decode(data),
'headers': <String, String>{},
},
bodyType: const FullType(Uint8List),
headersType: const FullType(Map, [FullType(String), FullType(String)]),
serializers: Serializers(),
),
getCacheParameters: getCacheParameters,
disableTimeout: disableTimeout,
);

/// Executes a HTTP request for binary content using a simplified [uri] based approach.
Future<void> wrapUri({
required Account account,
required Uri uri,
required UnwrapCallback<Uint8List, Uint8List>? unwrap,
required BehaviorSubject<Result<Uint8List>> subject,
}) {
final headers = account.getAuthorizationHeaders(uri);

return wrapBinary(
account: account,
cacheKey: uri.toString(),
getCacheParameters: () async {
final response = await account.client.executeRawRequest(
'HEAD',
uri,
headers: headers,
);

return CacheParameters.parseHeaders(response.headers);
},
rawResponse: DynamiteRawResponse<Uint8List, Map<String, String>>(
response: account.client.executeRawRequest(
'GET',
uri,
headers: headers,
validStatuses: const {200, 201},
),
bodyType: const FullType(Uint8List),
headersType: const FullType(Map, [FullType(String), FullType(String)]),
serializers: Serializers(),
),
unwrap: unwrap,
subject: subject,
);
}

/// Executes a generic request.
///
/// This method is only meant to be used in testing.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class NeonCustomBackground extends StatelessWidget {
return Stack(
children: [
Positioned.fill(
child: NeonUrlImage(
child: NeonUriImage(
uri: Uri.parse(theme.background),
fit: BoxFit.cover,
),
Expand Down
2 changes: 1 addition & 1 deletion packages/neon_framework/lib/src/widgets/drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class NeonDrawerHeader extends StatelessWidget {
),
),
Flexible(
child: NeonUrlImage(
child: NeonUriImage(
uri: Uri.parse(theme.logo),
),
),
Expand Down
Loading

0 comments on commit 79d20d0

Please sign in to comment.