mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-09 14:18:11 +02:00
v1.5.0: UI rework, multi-progress tracking, performance optimizations
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A collapsing header widget
|
||||
/// Title collapses from large to small when scrolling
|
||||
class CollapsingHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final bool showBackButton;
|
||||
final Widget? infoCard;
|
||||
final List<Widget> slivers;
|
||||
|
||||
const CollapsingHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.showBackButton = false,
|
||||
this.infoCard,
|
||||
required this.slivers,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 140,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
)
|
||||
: null,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final expandRatio = _calculateExpandRatio(constraints, topPadding);
|
||||
final animation = AlwaysStoppedAnimation(expandRatio);
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
padding: EdgeInsets.only(
|
||||
left: Tween<double>(begin: showBackButton ? 56 : 24, end: 24).evaluate(animation),
|
||||
bottom: Tween<double>(begin: 16, end: 24).evaluate(animation),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: Tween<double>(begin: 20, end: 28).evaluate(animation),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Info card if provided
|
||||
if (infoCard != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: infoCard,
|
||||
),
|
||||
),
|
||||
|
||||
// Content slivers
|
||||
...slivers,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateExpandRatio(BoxConstraints constraints, double topPadding) {
|
||||
final maxHeight = 140;
|
||||
final minHeight = kToolbarHeight + topPadding;
|
||||
final currentHeight = constraints.maxHeight;
|
||||
final expandRatio = (currentHeight - minHeight) / (maxHeight - minHeight);
|
||||
return expandRatio.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Section header for settings
|
||||
class SettingsSection extends StatelessWidget {
|
||||
final String title;
|
||||
const SettingsSection({super.key, required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Info card widget (like version info)
|
||||
class InfoCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const InfoCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -127,21 +127,82 @@ class UpdateDialog extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Format changelog - clean up markdown
|
||||
/// Format changelog - clean up markdown and extract relevant content
|
||||
String _formatChangelog(String changelog) {
|
||||
// Remove markdown headers but keep content
|
||||
var formatted = changelog
|
||||
.replaceAll(RegExp(r'^#{1,6}\s*', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'\*\*([^*]+)\*\*'), r'$1') // Remove bold
|
||||
.replaceAll(RegExp(r'`([^`]+)`'), r'$1') // Remove code
|
||||
.trim();
|
||||
// Try to extract just the changelog section (between "What's New" and "Downloads" or "---")
|
||||
var content = changelog;
|
||||
|
||||
// Limit length
|
||||
if (formatted.length > 1000) {
|
||||
formatted = '${formatted.substring(0, 1000)}...';
|
||||
// Find content after "What's New" header
|
||||
final whatsNewMatch = RegExp(r"###?\s*What'?s\s*New\s*\n", caseSensitive: false).firstMatch(content);
|
||||
if (whatsNewMatch != null) {
|
||||
content = content.substring(whatsNewMatch.end);
|
||||
}
|
||||
|
||||
return formatted;
|
||||
// Cut off at "Downloads" section or horizontal rule
|
||||
final cutoffMatch = RegExp(r'\n---|\n###?\s*Downloads', caseSensitive: false).firstMatch(content);
|
||||
if (cutoffMatch != null) {
|
||||
content = content.substring(0, cutoffMatch.start);
|
||||
}
|
||||
|
||||
// Process line by line for better formatting
|
||||
final lines = content.split('\n');
|
||||
final formattedLines = <String>[];
|
||||
String? currentSection;
|
||||
|
||||
for (var line in lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty) continue;
|
||||
|
||||
// Check if it's a section header (### Added, ### Fixed, etc.)
|
||||
final sectionMatch = RegExp(r'^#{1,3}\s*(.+)$').firstMatch(line);
|
||||
if (sectionMatch != null) {
|
||||
currentSection = sectionMatch.group(1)?.trim();
|
||||
if (currentSection != null && currentSection.isNotEmpty) {
|
||||
if (formattedLines.isNotEmpty) formattedLines.add('');
|
||||
formattedLines.add('$currentSection:');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a list item
|
||||
final listMatch = RegExp(r'^[-*]\s+(.+)$').firstMatch(line);
|
||||
if (listMatch != null) {
|
||||
var itemText = listMatch.group(1) ?? '';
|
||||
// Remove bold markdown
|
||||
itemText = itemText.replaceAllMapped(
|
||||
RegExp(r'\*\*([^*]+)\*\*'),
|
||||
(m) => m.group(1) ?? ''
|
||||
);
|
||||
// Remove code markdown
|
||||
itemText = itemText.replaceAllMapped(
|
||||
RegExp(r'`([^`]+)`'),
|
||||
(m) => m.group(1) ?? ''
|
||||
);
|
||||
formattedLines.add('• $itemText');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a sub-item (indented list)
|
||||
final subListMatch = RegExp(r'^\s+[-*]\s+(.+)$').firstMatch(line);
|
||||
if (subListMatch != null) {
|
||||
var itemText = subListMatch.group(1) ?? '';
|
||||
itemText = itemText.replaceAllMapped(
|
||||
RegExp(r'\*\*([^*]+)\*\*'),
|
||||
(m) => m.group(1) ?? ''
|
||||
);
|
||||
formattedLines.add(' - $itemText');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var formatted = formattedLines.join('\n').trim();
|
||||
|
||||
// Limit length
|
||||
if (formatted.length > 2000) {
|
||||
formatted = '${formatted.substring(0, 2000)}...';
|
||||
}
|
||||
|
||||
return formatted.isEmpty ? 'See release notes for details.' : formatted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user