diff --git a/lib/media/media_server_client.dart b/lib/media/media_server_client.dart index 52d29833..5e8bdd7b 100644 --- a/lib/media/media_server_client.dart +++ b/lib/media/media_server_client.dart @@ -332,7 +332,7 @@ abstract class MediaServerClient { // ── Images ─────────────────────────────────────────────────────── /// Resolve a backend-relative thumbnail path to a fully-qualified URL ready - /// for `cached_network_image`. Returns an empty string for null/empty + /// for cached image providers. Returns an empty string for null/empty /// inputs. /// /// When [width]/[height] are provided, the implementation should request diff --git a/lib/profiles/profile_avatar.dart b/lib/profiles/profile_avatar.dart index 8b3a8188..00ac669b 100644 --- a/lib/profiles/profile_avatar.dart +++ b/lib/profiles/profile_avatar.dart @@ -1,7 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../services/image_cache_service.dart'; import '../utils/initials_palette.dart'; import '../widgets/app_icon.dart'; import 'profile.dart'; @@ -67,9 +68,10 @@ class ProfileAvatar extends StatelessWidget { if (thumb != null && thumb.isNotEmpty) { return CachedNetworkImage( imageUrl: thumb, + cacheManager: PlexImageCacheManager.instance, fit: BoxFit.cover, placeholder: (_, _) => _initialFallback(theme, p), - errorWidget: (_, _, _) => _initialFallback(theme, p), + errorBuilder: (_, _, _) => _initialFallback(theme, p), ); } return _initialFallback(theme, p); diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 0499bb47..f036c65d 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -10,7 +10,7 @@ import '../focus/focusable_action_bar.dart'; import '../focus/input_mode_tracker.dart'; import '../focus/key_event_utils.dart'; import '../utils/global_key_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import '../services/image_cache_service.dart'; import '../media/media_item.dart'; @@ -1498,7 +1498,7 @@ class _DiscoverScreenState extends State memCacheHeight: memHeight, placeholder: (context, url) => ColoredBox(color: Theme.of(context).colorScheme.surfaceContainerHighest), - errorWidget: (context, url, error) => + errorBuilder: (context, error, stackTrace) => ColoredBox(color: Theme.of(context).colorScheme.surfaceContainerHighest), ), ); @@ -1572,7 +1572,7 @@ class _DiscoverScreenState extends State memCacheWidth: (400 * dpr).clamp(200, 800).round(), alignment: isLargeScreen ? Alignment.bottomLeft : Alignment.bottomCenter, placeholder: (context, url) => const SizedBox.shrink(), - errorWidget: (context, url, error) { + errorBuilder: (context, error, stackTrace) { // Fallback to text if logo fails to load final theme = Theme.of(context); final colorScheme = theme.colorScheme; diff --git a/lib/screens/libraries/tabs/library_browse_tab.dart b/lib/screens/libraries/tabs/library_browse_tab.dart index 8bdef6e2..0a57d82e 100644 --- a/lib/screens/libraries/tabs/library_browse_tab.dart +++ b/lib/screens/libraries/tabs/library_browse_tab.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import '../../../media/library_first_character.dart'; import '../../../media/library_query.dart'; import '../../../media/media_item.dart'; diff --git a/lib/screens/livetv/program_details_sheet.dart b/lib/screens/livetv/program_details_sheet.dart index 6263235d..09219f5c 100644 --- a/lib/screens/livetv/program_details_sheet.dart +++ b/lib/screens/livetv/program_details_sheet.dart @@ -1,4 +1,4 @@ -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -168,7 +168,7 @@ class _ProgramDetailsSheetContentState extends State<_ProgramDetailsSheetContent height: 120, fit: BoxFit.cover, memCacheHeight: (120 * MediaQuery.devicePixelRatioOf(context)).round(), - errorWidget: (_, _, _) => const SizedBox.shrink(), + errorBuilder: (_, _, _) => const SizedBox.shrink(), ), ), ), diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index fe531ff1..abe74aca 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../services/image_cache_service.dart'; @@ -822,7 +822,7 @@ class _MediaDetailScreenState extends State fit: BoxFit.cover, memCacheHeight: memCacheHeight, placeholder: (context, url) => const PlaceholderContainer(), - errorWidget: (context, url, error) => _buildHeroNetworkArtwork( + errorBuilder: (context, error, stackTrace) => _buildHeroNetworkArtwork( context, client: client, artworkPaths: artworkPaths, @@ -1571,7 +1571,7 @@ class _MediaDetailScreenState extends State fit: BoxFit.cover, memCacheWidth: memWidth, placeholder: (context, url) => const PlaceholderContainer(), - errorWidget: (context, url, error) => const PlaceholderContainer(), + errorBuilder: (context, error, stackTrace) => const PlaceholderContainer(), ), ); } @@ -2528,7 +2528,8 @@ class _MediaDetailScreenState extends State alignment: Alignment.centerLeft, memCacheWidth: (400 * dpr).clamp(200, 800).round(), placeholder: (context, url) => const SizedBox.shrink(), - errorWidget: (context, url, error) => _buildTitleText(context, metadata.displayTitle), + errorBuilder: (context, error, stackTrace) => + _buildTitleText(context, metadata.displayTitle), ), sigma: 10, clip: false, diff --git a/lib/services/image_cache_service.dart b/lib/services/image_cache_service.dart index ad79f81e..30bc5152 100644 --- a/lib/services/image_cache_service.dart +++ b/lib/services/image_cache_service.dart @@ -1,102 +1,41 @@ -import 'dart:io'; - -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +// CE's public conditional export hides the IO-only httpClientFactory parameter +// behind a narrower unsupported-platform stub. +// ignore: implementation_imports +import 'package:cached_network_image_ce/src/cache/default_cache_manager.dart' as ce_cache; import 'package:http/http.dart' as http; -import '../utils/platform_detector.dart'; import '../utils/media_server_http_client.dart'; -/// Custom cache manager for media-server image transcoding with HTTP/2 -/// multiplexing. Used for both Plex and Jellyfin artwork (the class name -/// predates Jellyfin support — it's backend-neutral). +/// Shared cache manager for media-server image artwork. Used for both Plex and +/// Jellyfin artwork (the class name predates Jellyfin support — it's +/// backend-neutral). /// /// Uses the platform-native HTTP client so iOS/macOS (CupertinoClient) and /// Android (CronetClient) benefit from HTTP/2 connection multiplexing — /// many concurrent image downloads over a single connection instead of /// being limited to a handful of HTTP/1.1 connections. -class PlexImageCacheManager extends CacheManager with ImageCacheManager { - static const _key = 'plexImageCache'; - +class PlexImageCacheManager extends ce_cache.DefaultCacheManager { static final PlexImageCacheManager instance = PlexImageCacheManager._(); - PlexImageCacheManager._() : super(_buildConfig()); - - static Config _buildConfig() { - final fileService = _HttpFileService(httpClient.inner); - // tvOS has no sqflite plugin, so force the JSON cache-info repo there. - // On other platforms we let flutter_cache_manager pick its default repo. - if (PlatformDetector.isAppleTV()) { - return Config( - _key, + PlexImageCacheManager._() + : super( stalePeriod: const Duration(days: 14), maxNrOfCacheObjects: 3000, - fileService: fileService, - repo: JsonCacheInfoRepository(databaseName: _key), + httpClientFactory: () => _SharedHttpClient(httpClient.inner), ); - } - return Config(_key, stalePeriod: const Duration(days: 14), maxNrOfCacheObjects: 3000, fileService: fileService); - } } -class _HttpFileService extends FileService { - final http.Client _client; +/// CE closes each factory-created client after a download. Wrap the app-wide +/// shared client so image requests reuse its platform transport without +/// transferring ownership of its lifecycle. +class _SharedHttpClient extends http.BaseClient { + final http.Client _inner; - _HttpFileService(this._client); + _SharedHttpClient(this._inner); @override - Future get(String url, {Map? headers}) async { - final request = http.Request('GET', Uri.parse(url)); - if (headers != null) request.headers.addAll(headers); - final response = await _client.send(request); - return _HttpGetResponse(response); - } -} + Future send(http.BaseRequest request) => _inner.send(request); -class _HttpGetResponse implements FileServiceResponse { - final http.StreamedResponse _response; - final DateTime _receivedTime = DateTime.now(); - - _HttpGetResponse(this._response); - - @override - Stream> get content => _response.stream; - - @override - int? get contentLength { - final value = _response.headers[HttpHeaders.contentLengthHeader]; - return value != null ? int.tryParse(value) : null; - } - - @override - int get statusCode => _response.statusCode; - - @override - DateTime get validTill { - var ageDuration = const Duration(days: 7); - final controlHeader = _response.headers[HttpHeaders.cacheControlHeader]; - if (controlHeader != null) { - for (final setting in controlHeader.split(',')) { - final s = setting.trim().toLowerCase(); - if (s == 'no-cache') ageDuration = Duration.zero; - if (s.startsWith('max-age=')) { - final secs = int.tryParse(s.split('=')[1]) ?? 0; - if (secs > 0) ageDuration = Duration(seconds: secs); - } - } - } - return _receivedTime.add(ageDuration); - } - - @override - String? get eTag => _response.headers[HttpHeaders.etagHeader]; - @override - String get fileExtension { - final contentTypeHeader = _response.headers[HttpHeaders.contentTypeHeader]; - if (contentTypeHeader != null) { - final ct = ContentType.parse(contentTypeHeader); - return '.${ct.subType}'; - } - return ''; - } + void close() {} } diff --git a/lib/services/jellyfin_trickplay_service.dart b/lib/services/jellyfin_trickplay_service.dart index 61718aed..bd6776d2 100644 --- a/lib/services/jellyfin_trickplay_service.dart +++ b/lib/services/jellyfin_trickplay_service.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'dart:ui'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/painting.dart' show ImageConfiguration, ImageProvider, ImageStreamListener; import '../media/media_source_info.dart'; @@ -36,7 +36,7 @@ class JellyfinTrickplayService implements ScrubPreviewSource { /// Provider cache by sheet index: scrubbing typically dwells on one sheet /// for many hover events, so we reuse the same wrapper rather than - /// reallocating each frame. cached_network_image's own cache is keyed by + /// reallocating each frame. The image cache itself is keyed by /// URL, but the wrapper object itself isn't free. final Map _providerCache = {}; diff --git a/lib/widgets/optimized_media_image.dart b/lib/widgets/optimized_media_image.dart index 7c9fad87..80f00eb3 100644 --- a/lib/widgets/optimized_media_image.dart +++ b/lib/widgets/optimized_media_image.dart @@ -1,11 +1,15 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:ui'; + +import 'package:cached_network_image_ce/cached_network_image.dart'; +import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; -import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import '../services/image_cache_service.dart'; +import 'package:plezy/widgets/app_icon.dart'; + import '../media/media_server_client.dart'; +import '../services/image_cache_service.dart'; import '../utils/app_logger.dart'; import '../utils/media_image_helper.dart'; import '../utils/obfuscation_utils.dart'; @@ -356,6 +360,6 @@ class OptimizedMediaImage extends StatelessWidget { // so the URL hash alone uniquely identifies the bytes on disk. Including // mem-cache dimensions here would re-introduce churn on every pixel of // window resize and defeat getMemCacheDimensions' bucketing. - return 'plex_optimized_${imageUrl.hashCode}'; + return 'plex_optimized_${sha1.convert(utf8.encode(imageUrl))}'; } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 797593e4..9e7875d2 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,7 +15,6 @@ import package_info_plus import screen_retriever_macos import sentry_flutter import shared_preferences_foundation -import sqflite_darwin import sqlite3_flutter_libs import universal_gamepad import url_launcher_macos @@ -33,7 +32,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) GamepadPlugin.register(with: registry.registrar(forPlugin: "GamepadPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a5265c80..f40434a7 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -29,9 +29,6 @@ PODS: - Flutter - FlutterMacOS - Sparkle (2.9.0) - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - sqlite3 (3.52.0): - sqlite3/common (= 3.52.0) - sqlite3/common (3.52.0) @@ -79,7 +76,6 @@ DEPENDENCIES: - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) - universal_gamepad (from `Flutter/ephemeral/.symlinks/plugins/universal_gamepad/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -117,8 +113,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin sqlite3_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin universal_gamepad: @@ -145,7 +139,6 @@ SPEC CHECKSUMS: sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb Sparkle: f4355f9ebbe9b7d932df4980d70f13922ac97b2a - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab universal_gamepad: 8922f1f238f62d6847de887228976d5b572b57da diff --git a/pubspec.lock b/pubspec.lock index 0f9d09ba..7da2e12e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -150,30 +150,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.4" - cached_network_image: + cached_network_image_ce: dependency: "direct main" description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + name: cached_network_image_ce + sha256: d4701fbbdf508de831e847fd1d49c644ffa6191c6018c416ab6f66bbe1ad5fff url: "https://pub.dev" source: hosted - version: "3.4.1" - cached_network_image_platform_interface: + version: "4.6.4" + cached_network_image_platform_interface_ce: dependency: transitive description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + name: cached_network_image_platform_interface_ce + sha256: "95343645becaeac01dd6eeb9f3a85448b6076d211010ff1973e8bbfd840b571d" url: "https://pub.dev" source: hosted - version: "4.1.1" - cached_network_image_web: + version: "5.2.0" + cached_network_image_web_ce: dependency: transitive description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + name: cached_network_image_web_ce + sha256: "6bcd8608d4ace7c35625f09d971ce68c7a6581a4631b0fb6ceb2909b2c2e17a8" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "2.1.1" characters: dependency: transitive description: @@ -443,14 +443,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -506,6 +498,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hive_ce: + dependency: transitive + description: + name: hive_ce + sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418" + url: "https://pub.dev" + source: hosted + version: "2.19.3" hooks: dependency: transitive description: @@ -586,6 +586,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + isolate_channel: + dependency: transitive + description: + name: isolate_channel + sha256: a9d3d620695bc984244dafae00b95e4319d6974b2d77f4b9e1eb4f2efe099094 + url: "https://pub.dev" + source: hosted + version: "0.6.1" jni: dependency: transitive description: @@ -940,14 +948,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" saf_stream: dependency: "direct main" description: @@ -1155,46 +1155,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.dev" - source: hosted - version: "2.4.2+3" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" sqlite3: dependency: transitive description: @@ -1251,14 +1211,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6be6d67e..2aada2a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,8 +13,7 @@ dependencies: intl: ^0.20.2 json_annotation: ^4.9.0 shared_preferences: ^2.5.4 - cached_network_image: ^3.4.1 - flutter_cache_manager: ^3.4.1 + cached_network_image_ce: 4.6.4 http: ^1.2.0 url_launcher: ^6.3.0 uuid: ^4.4.0 diff --git a/test/services/jellyfin_trickplay_service_test.dart b/test/services/jellyfin_trickplay_service_test.dart index 5ac0bd79..27205011 100644 --- a/test/services/jellyfin_trickplay_service_test.dart +++ b/test/services/jellyfin_trickplay_service_test.dart @@ -40,7 +40,7 @@ TrickplayInfo _info({ ); /// Stub that returns a constant 1×1 transparent image for any URL — keeps -/// tests off path_provider / cached_network_image's disk cache. +/// tests off path_provider / the image disk cache. ImageProvider _fakeSheet(String _) => MemoryImage(Uint8List.fromList(const [0])); void main() {