mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-19 14:07:26 +02:00
165 lines
4.8 KiB
Dart
165 lines
4.8 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:spotiflac_android/services/metadata_autofill_service.dart';
|
||
import 'package:spotiflac_android/services/metadata_cover_resources.dart';
|
||
import 'package:spotiflac_android/utils/isrc_utils.dart';
|
||
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
|
||
|
||
class MetadataCandidateTile extends StatelessWidget {
|
||
final Map<String, dynamic> candidate;
|
||
final VoidCallback onTap;
|
||
|
||
const MetadataCandidateTile({
|
||
super.key,
|
||
required this.candidate,
|
||
required this.onTap,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final title = (candidate['name'] ?? candidate['title'] ?? '')
|
||
.toString()
|
||
.trim();
|
||
final artists = (candidate['artists'] ?? candidate['artist'] ?? '')
|
||
.toString()
|
||
.trim();
|
||
final album = (candidate['album_name'] ?? candidate['album'] ?? '')
|
||
.toString()
|
||
.trim();
|
||
final date = candidate['release_date']?.toString().trim() ?? '';
|
||
final isrc = candidate['isrc']?.toString().trim() ?? '';
|
||
final subtitle = [
|
||
artists,
|
||
album,
|
||
if (date.isNotEmpty) date,
|
||
if (isrc.isNotEmpty) 'ISRC ${formatIsrcForDisplay(isrc)}',
|
||
].where((value) => value.isNotEmpty).join('\n');
|
||
return ListTile(
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||
leading: _MetadataCandidateArtwork(
|
||
coverUrl: MetadataAutofillService().coverUrl(candidate),
|
||
),
|
||
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||
subtitle: subtitle.isEmpty
|
||
? null
|
||
: Text(subtitle, maxLines: 4, overflow: TextOverflow.ellipsis),
|
||
trailing: const Icon(Icons.chevron_right),
|
||
onTap: onTap,
|
||
);
|
||
}
|
||
}
|
||
|
||
class _MetadataCandidateArtwork extends StatefulWidget {
|
||
final String? coverUrl;
|
||
|
||
const _MetadataCandidateArtwork({required this.coverUrl});
|
||
|
||
@override
|
||
State<_MetadataCandidateArtwork> createState() =>
|
||
_MetadataCandidateArtworkState();
|
||
}
|
||
|
||
class _MetadataCandidateArtworkState extends State<_MetadataCandidateArtwork> {
|
||
final _covers = MetadataCoverResources();
|
||
String? _coverPath;
|
||
String? _tempDir;
|
||
({int width, int height, int bytes})? _details;
|
||
int _generation = 0;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
if (widget.coverUrl?.isNotEmpty == true) {
|
||
unawaited(_loadCover());
|
||
}
|
||
}
|
||
|
||
Future<void> _loadCover() async {
|
||
final generation = ++_generation;
|
||
final preview = await _covers.download(widget.coverUrl!);
|
||
if (!mounted || generation != _generation) {
|
||
await _covers.release(preview?.tempDir);
|
||
return;
|
||
}
|
||
setState(() {
|
||
_coverPath = preview?.path;
|
||
_tempDir = preview?.tempDir;
|
||
_details = preview?.details;
|
||
});
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(covariant _MetadataCandidateArtwork oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (widget.coverUrl == oldWidget.coverUrl) return;
|
||
_generation++;
|
||
unawaited(_covers.release(_tempDir));
|
||
_tempDir = null;
|
||
_coverPath = null;
|
||
_details = null;
|
||
if (widget.coverUrl?.isNotEmpty == true) unawaited(_loadCover());
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
unawaited(_covers.dispose());
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final coverUrl = widget.coverUrl;
|
||
final details = _details;
|
||
final image = _coverPath != null
|
||
? Image.file(
|
||
File(_coverPath!),
|
||
width: 56,
|
||
height: 56,
|
||
fit: BoxFit.cover,
|
||
cacheWidth: (56 * MediaQuery.devicePixelRatioOf(context)).round(),
|
||
cacheHeight: (56 * MediaQuery.devicePixelRatioOf(context)).round(),
|
||
filterQuality: FilterQuality.low,
|
||
)
|
||
: coverUrl != null
|
||
? CachedCoverImage(
|
||
imageUrl: coverUrl,
|
||
width: 56,
|
||
height: 56,
|
||
fit: BoxFit.cover,
|
||
)
|
||
: Container(
|
||
width: 56,
|
||
height: 56,
|
||
color: colorScheme.surfaceContainerHighest,
|
||
child: const Icon(Icons.music_note),
|
||
);
|
||
|
||
return SizedBox(
|
||
width: 72,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
ClipRRect(borderRadius: BorderRadius.circular(10), child: image),
|
||
if (details != null) ...[
|
||
const SizedBox(height: 3),
|
||
Text(
|
||
'${details.width}×${details.height} · '
|
||
'${(details.bytes / 1024).toStringAsFixed(1)} KB',
|
||
maxLines: 2,
|
||
textAlign: TextAlign.center,
|
||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||
color: colorScheme.primary,
|
||
fontSize: 9,
|
||
height: 1.1,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|