198 lines
6.2 KiB
Dart
198 lines
6.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:octo_image/octo_image.dart';
|
|
|
|
import '../models/jellyfin_models.dart';
|
|
import '../services/album_image_provider.dart';
|
|
|
|
typedef ImageProviderCallback = void Function(ImageProvider? imageProvider);
|
|
|
|
/// This widget provides the default look for album images throughout Finamp -
|
|
/// Aspect ratio 1 with a circular border radius of 4. If you don't want these
|
|
/// customisations, use [BareAlbumImage] or get an [ImageProvider] directly
|
|
/// through [AlbumImageProvider.init].
|
|
class AlbumImage extends StatelessWidget {
|
|
const AlbumImage({
|
|
Key? key,
|
|
this.item,
|
|
this.imageProviderCallback,
|
|
this.itemsToPrecache,
|
|
}) : super(key: key);
|
|
|
|
/// The item to get an image for.
|
|
final BaseItemDto? item;
|
|
|
|
/// A callback to get the image provider once it has been fetched.
|
|
final ImageProviderCallback? imageProviderCallback;
|
|
|
|
/// A list of items to precache
|
|
final List<BaseItemDto>? itemsToPrecache;
|
|
|
|
static final BorderRadius borderRadius = BorderRadius.circular(4);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (item == null || item!.imageId == null) {
|
|
if (imageProviderCallback != null) {
|
|
imageProviderCallback!(null);
|
|
}
|
|
|
|
return ClipRRect(
|
|
borderRadius: borderRadius,
|
|
child: const AspectRatio(
|
|
aspectRatio: 1,
|
|
child: _AlbumImageErrorPlaceholder(),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ClipRRect(
|
|
borderRadius: borderRadius,
|
|
child: AspectRatio(
|
|
aspectRatio: 1,
|
|
child: LayoutBuilder(builder: (context, constraints) {
|
|
// LayoutBuilder (and other pixel-related stuff in Flutter) returns logical pixels instead of physical pixels.
|
|
// While this is great for doing layout stuff, we want to get images that are the right size in pixels.
|
|
// Logical pixels aren't the same as the physical pixels on the device, they're quite a bit bigger.
|
|
// If we use logical pixels for the image request, we'll get a smaller image than we want.
|
|
// Because of this, we convert the logical pixels to physical pixels by multiplying by the device's DPI.
|
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
|
final int physicalWidth =
|
|
(constraints.maxWidth * mediaQuery.devicePixelRatio).toInt();
|
|
final int physicalHeight =
|
|
(constraints.maxHeight * mediaQuery.devicePixelRatio).toInt();
|
|
|
|
return BareAlbumImage(
|
|
item: item!,
|
|
maxWidth: physicalWidth,
|
|
maxHeight: physicalHeight,
|
|
imageProviderCallback: imageProviderCallback,
|
|
itemsToPrecache: itemsToPrecache,
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// An [AlbumImage] without any of the padding or media size detection.
|
|
class BareAlbumImage extends StatefulWidget {
|
|
const BareAlbumImage({
|
|
Key? key,
|
|
required this.item,
|
|
this.maxWidth,
|
|
this.maxHeight,
|
|
this.errorBuilder,
|
|
this.placeholderBuilder,
|
|
this.imageProviderCallback,
|
|
this.itemsToPrecache,
|
|
}) : super(key: key);
|
|
|
|
final BaseItemDto item;
|
|
final int? maxWidth;
|
|
final int? maxHeight;
|
|
final WidgetBuilder? placeholderBuilder;
|
|
final OctoErrorBuilder? errorBuilder;
|
|
final ImageProviderCallback? imageProviderCallback;
|
|
|
|
/// A list of items to precache
|
|
final List<BaseItemDto>? itemsToPrecache;
|
|
|
|
@override
|
|
State<BareAlbumImage> createState() => _BareAlbumImageState();
|
|
}
|
|
|
|
class _BareAlbumImageState extends State<BareAlbumImage> {
|
|
late Future<ImageProvider?> _albumImageContentFuture;
|
|
late WidgetBuilder _placeholderBuilder;
|
|
late OctoErrorBuilder _errorBuilder;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_albumImageContentFuture = AlbumImageProvider.init(
|
|
widget.item,
|
|
maxWidth: widget.maxWidth,
|
|
maxHeight: widget.maxHeight,
|
|
itemsToPrecache: widget.itemsToPrecache,
|
|
context: context,
|
|
);
|
|
_placeholderBuilder = widget.placeholderBuilder ??
|
|
(context) => Container(
|
|
color: Theme.of(context).cardColor,
|
|
);
|
|
_errorBuilder = widget.errorBuilder ??
|
|
(context, _, __) => const _AlbumImageErrorPlaceholder();
|
|
}
|
|
|
|
// We need to do this so that the image changes when dependencies change, such
|
|
// as when used in the player screen.
|
|
@override
|
|
void didUpdateWidget(BareAlbumImage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.item.imageId != oldWidget.item.imageId ||
|
|
widget.maxWidth != oldWidget.maxWidth ||
|
|
widget.maxHeight != oldWidget.maxHeight ||
|
|
widget.itemsToPrecache != oldWidget.itemsToPrecache) {
|
|
_albumImageContentFuture = AlbumImageProvider.init(
|
|
widget.item,
|
|
maxWidth: widget.maxWidth,
|
|
maxHeight: widget.maxHeight,
|
|
itemsToPrecache: widget.itemsToPrecache,
|
|
context: context,
|
|
);
|
|
}
|
|
_placeholderBuilder = widget.placeholderBuilder ??
|
|
(context) => Container(
|
|
color: Theme.of(context).cardColor,
|
|
);
|
|
_errorBuilder = widget.errorBuilder ??
|
|
(context, _, __) => const _AlbumImageErrorPlaceholder();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder<ImageProvider?>(
|
|
future: _albumImageContentFuture,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData) {
|
|
if (widget.imageProviderCallback != null) {
|
|
widget.imageProviderCallback!(snapshot.data!);
|
|
}
|
|
|
|
return OctoImage(
|
|
image: snapshot.data!,
|
|
fit: BoxFit.cover,
|
|
placeholderBuilder: _placeholderBuilder,
|
|
errorBuilder: _errorBuilder,
|
|
);
|
|
}
|
|
|
|
if (snapshot.hasError) {
|
|
if (widget.imageProviderCallback != null) {
|
|
widget.imageProviderCallback!(null);
|
|
}
|
|
return const _AlbumImageErrorPlaceholder();
|
|
}
|
|
|
|
if (widget.imageProviderCallback != null) {
|
|
widget.imageProviderCallback!(null);
|
|
}
|
|
|
|
return Builder(builder: _placeholderBuilder);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AlbumImageErrorPlaceholder extends StatelessWidget {
|
|
const _AlbumImageErrorPlaceholder({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
color: Theme.of(context).cardColor,
|
|
child: const Icon(Icons.album),
|
|
);
|
|
}
|
|
}
|