mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
162 lines
4.4 KiB
Dart
162 lines
4.4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:spotiflac_android/theme/app_tokens.dart';
|
|
|
|
/// Square extension icon with a tinted fallback.
|
|
///
|
|
/// Both the store and the installed-extensions page drew this by hand with the
|
|
/// same 44dp box, radius and fallback-icon logic, differing only in whether the
|
|
/// image came from a file or the network.
|
|
class ExtensionAvatar extends StatelessWidget {
|
|
const ExtensionAvatar({
|
|
super.key,
|
|
required this.fallbackIcon,
|
|
this.filePath,
|
|
this.imageUrl,
|
|
this.background,
|
|
this.foreground,
|
|
});
|
|
|
|
/// Shown when no image is available or the image fails to load.
|
|
final IconData fallbackIcon;
|
|
|
|
/// Local icon shipped inside an installed extension.
|
|
final String? filePath;
|
|
|
|
/// Remote icon from the store registry.
|
|
final String? imageUrl;
|
|
|
|
final Color? background;
|
|
final Color? foreground;
|
|
|
|
static const double size = 44;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = context.tokens;
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final borderRadius = tokens.borderRadiusCover;
|
|
final fallback = Icon(
|
|
fallbackIcon,
|
|
color: foreground ?? colorScheme.onPrimaryContainer,
|
|
);
|
|
|
|
Widget? image;
|
|
if (filePath != null && filePath!.isNotEmpty) {
|
|
image = Image.file(
|
|
File(filePath!),
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, _, _) => fallback,
|
|
);
|
|
} else if (imageUrl != null && imageUrl!.isNotEmpty) {
|
|
image = Image.network(
|
|
imageUrl!,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, _, _) => fallback,
|
|
loadingBuilder: (context, child, progress) {
|
|
if (progress == null) return child;
|
|
return const Center(
|
|
child: SizedBox.square(
|
|
dimension: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
color: background ?? colorScheme.primaryContainer,
|
|
borderRadius: borderRadius,
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: image ?? fallback,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Row shell shared by the extension store and the installed-extensions list:
|
|
/// avatar, title/subtitle column, trailing control, and the group divider.
|
|
class ExtensionRow extends StatelessWidget {
|
|
const ExtensionRow({
|
|
super.key,
|
|
required this.avatar,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.showDivider,
|
|
this.trailing,
|
|
this.onTap,
|
|
});
|
|
|
|
final Widget avatar;
|
|
|
|
/// A widget rather than a string: the store row appends a version pill.
|
|
final Widget title;
|
|
|
|
final Widget subtitle;
|
|
final bool showDivider;
|
|
final Widget? trailing;
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = context.tokens;
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: tokens.gapLg,
|
|
vertical: tokens.gapMd,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
avatar,
|
|
SizedBox(width: tokens.gapLg),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
DefaultTextStyle.merge(
|
|
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
child: title,
|
|
),
|
|
SizedBox(height: tokens.gapXs / 2),
|
|
subtitle,
|
|
],
|
|
),
|
|
),
|
|
?trailing,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (showDivider)
|
|
Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
// Aligns with the text column: avatar width + its trailing gap.
|
|
indent: ExtensionAvatar.size + tokens.gapLg * 2,
|
|
endIndent: tokens.gapLg,
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|