mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
- single-track convert sheet now reuses BatchConvertSheet (confirmLabelBuilder + sourceIsLossless params) instead of a full inline copy - provider priority page migrated to PrioritySettingsScaffold; shared showDiscardChangesDialog and ReorderablePriorityItem replace per-page copies - home tab search rows unified into one _SearchResultRowItem; _parseTrack copies rebuilt on Track.fromBackendMap with only the load-bearing local overrides kept; loading/error scaffold extracted - PlayerArtwork widget shared by now-playing and mini player - LocalOrNetworkCoverImage dispatches file vs network cover in one place (playlist picker, queue nav, library folder)
192 lines
5.7 KiB
Dart
192 lines
5.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
|
|
|
class CachedCoverImage extends StatelessWidget {
|
|
static const int _defaultMinCacheExtent = 64;
|
|
static const int _defaultMaxCacheExtent = 512;
|
|
|
|
final String imageUrl;
|
|
final double? width;
|
|
final double? height;
|
|
final BoxFit fit;
|
|
final Alignment alignment;
|
|
final int? memCacheWidth;
|
|
final int? memCacheHeight;
|
|
final Widget Function(BuildContext, String, Object)? errorWidget;
|
|
final Widget Function(BuildContext, String)? placeholder;
|
|
final BorderRadius? borderRadius;
|
|
final bool resizeDiskCache;
|
|
final Duration fadeInDuration;
|
|
final Duration fadeOutDuration;
|
|
|
|
const CachedCoverImage({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.width,
|
|
this.height,
|
|
this.fit = BoxFit.cover,
|
|
this.alignment = Alignment.center,
|
|
this.memCacheWidth,
|
|
this.memCacheHeight,
|
|
this.errorWidget,
|
|
this.placeholder,
|
|
this.borderRadius,
|
|
this.resizeDiskCache = false,
|
|
this.fadeInDuration = Duration.zero,
|
|
this.fadeOutDuration = Duration.zero,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final autoMemCacheWidth =
|
|
memCacheWidth ?? _cacheExtentForLogicalSize(context, width);
|
|
final autoMemCacheHeight =
|
|
memCacheHeight ?? _cacheExtentForLogicalSize(context, height);
|
|
final diskCacheWidth = resizeDiskCache ? autoMemCacheWidth : null;
|
|
final diskCacheHeight = resizeDiskCache ? autoMemCacheHeight : null;
|
|
final image = CachedNetworkImage(
|
|
imageUrl: imageUrl,
|
|
width: width,
|
|
height: height,
|
|
fit: fit,
|
|
alignment: alignment,
|
|
memCacheWidth: autoMemCacheWidth,
|
|
memCacheHeight: autoMemCacheHeight,
|
|
maxWidthDiskCache: diskCacheWidth,
|
|
maxHeightDiskCache: diskCacheHeight,
|
|
cacheManager: CoverCacheManager.instance,
|
|
fadeInDuration: fadeInDuration,
|
|
fadeOutDuration: fadeOutDuration,
|
|
useOldImageOnUrlChange: true,
|
|
filterQuality: FilterQuality.low,
|
|
errorWidget: errorWidget,
|
|
placeholder: placeholder,
|
|
);
|
|
|
|
if (borderRadius != null) {
|
|
return ClipRRect(borderRadius: borderRadius!, child: image);
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
static int? _cacheExtentForLogicalSize(BuildContext context, double? size) {
|
|
if (size == null || !size.isFinite || size <= 0) return null;
|
|
final dpr = MediaQuery.devicePixelRatioOf(
|
|
context,
|
|
).clamp(1.0, 3.0).toDouble();
|
|
return (size * dpr)
|
|
.round()
|
|
.clamp(_defaultMinCacheExtent, _defaultMaxCacheExtent)
|
|
.toInt();
|
|
}
|
|
}
|
|
|
|
/// Renders [url] as a local file (when it's not an http/https URL) or as a
|
|
/// cached network image otherwise, with a shared [placeholder] used for the
|
|
/// local error state, the local not-ready frame, and the network
|
|
/// placeholder/error states alike.
|
|
class LocalOrNetworkCoverImage extends StatelessWidget {
|
|
final String url;
|
|
final double? width;
|
|
final double? height;
|
|
final BoxFit fit;
|
|
final BorderRadius? borderRadius;
|
|
final int? localCacheWidth;
|
|
final int? networkCacheWidth;
|
|
final Duration? fadeInDuration;
|
|
final Duration fadeOutDuration;
|
|
final String Function(String)? urlTransform;
|
|
final Widget Function(BuildContext) placeholder;
|
|
|
|
const LocalOrNetworkCoverImage({
|
|
super.key,
|
|
required this.url,
|
|
this.width,
|
|
this.height,
|
|
this.fit = BoxFit.cover,
|
|
this.borderRadius,
|
|
this.localCacheWidth,
|
|
this.networkCacheWidth,
|
|
this.fadeInDuration,
|
|
this.fadeOutDuration = Duration.zero,
|
|
this.urlTransform,
|
|
required this.placeholder,
|
|
});
|
|
|
|
bool get _isLocal =>
|
|
!url.startsWith('http://') && !url.startsWith('https://');
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLocal) {
|
|
final image = Image.file(
|
|
File(url),
|
|
width: width,
|
|
height: height,
|
|
fit: fit,
|
|
cacheWidth: localCacheWidth,
|
|
gaplessPlayback: true,
|
|
filterQuality: FilterQuality.low,
|
|
frameBuilder: fadeInDuration == null
|
|
? null
|
|
: (context, child, frame, wasSynchronouslyLoaded) {
|
|
final ready = wasSynchronouslyLoaded || frame != null;
|
|
if (fadeInDuration == Duration.zero) {
|
|
return ready ? child : placeholder(context);
|
|
}
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
placeholder(context),
|
|
AnimatedOpacity(
|
|
opacity: ready ? 1.0 : 0.0,
|
|
duration: fadeInDuration!,
|
|
curve: Curves.easeOutCubic,
|
|
child: child,
|
|
),
|
|
],
|
|
);
|
|
},
|
|
errorBuilder: (_, _, _) => placeholder(context),
|
|
);
|
|
return borderRadius == null
|
|
? image
|
|
: ClipRRect(borderRadius: borderRadius!, child: image);
|
|
}
|
|
|
|
return CachedCoverImage(
|
|
imageUrl: urlTransform?.call(url) ?? url,
|
|
width: width,
|
|
height: height,
|
|
fit: fit,
|
|
memCacheWidth: networkCacheWidth,
|
|
borderRadius: borderRadius,
|
|
fadeInDuration: fadeInDuration ?? Duration.zero,
|
|
fadeOutDuration: fadeOutDuration,
|
|
placeholder: (_, _) => placeholder(context),
|
|
errorWidget: (_, _, _) => placeholder(context),
|
|
);
|
|
}
|
|
}
|
|
|
|
CachedNetworkImageProvider cachedCoverImageProvider(String url) {
|
|
return CachedNetworkImageProvider(
|
|
url,
|
|
cacheManager: CoverCacheManager.instance,
|
|
);
|
|
}
|
|
|
|
int coverImageCacheExtent(
|
|
BuildContext context,
|
|
double logicalSize, {
|
|
int min = 64,
|
|
int max = 512,
|
|
}) {
|
|
final dpr = MediaQuery.devicePixelRatioOf(context).clamp(1.0, 3.0).toDouble();
|
|
return (logicalSize * dpr).round().clamp(min, max).toInt();
|
|
}
|