chore: init repository with project sources

This commit is contained in:
2025-10-30 10:29:33 +01:00
commit 65f1175ba7
97 changed files with 7762 additions and 0 deletions
@@ -0,0 +1,167 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/status.dart';
import '../data/movie_repository.dart';
import 'widgets/status_chip.dart';
class MovieListScreen extends ConsumerWidget {
const MovieListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final filter = ref.watch(movieFilterProvider);
final moviesAsync = ref.watch(moviesProvider);
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusChip(
selected: filter,
onChanged: (f) => ref.read(movieFilterProvider.notifier).state = f,
),
const SizedBox(height: 12),
Expanded(
child: moviesAsync.when(
data: (items) {
final itemsSorted = [...items]
..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
return RefreshIndicator(
onRefresh: () async => ref.invalidate(moviesProvider),
child: ListView.separated(
itemCount: itemsSorted.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final m = itemsSorted[i];
return ListTile(
tileColor: _statusBg(m.status, context),
leading: m.posterPath != null
? ClipRRect(
borderRadius: BorderRadius.circular(6),
child: CachedNetworkImage(
imageUrl:
'https://image.tmdb.org/t/p/w154${m.posterPath}',
width: 50,
height: 75,
fit: BoxFit.cover,
),
)
: const SizedBox(width: 50, height: 75),
title: Text(
m.releaseYear != null
? '${m.title} (${m.releaseYear})'
: m.title,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 56,
child: Align(
alignment: Alignment.topLeft,
child: FittedBox(
alignment: Alignment.topLeft,
fit: BoxFit.scaleDown,
child: _resolutionBadge(m.resolution, context),
),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
m.overview ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
isThreeLine: true,
onTap: () {
// TODO: Detailseite / Status ändern
},
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, st) => Center(
child: Text('Fehler: ${e.toString()}'),
),
),
),
],
),
);
}
IconData _statusIcon(ItemStatus s) {
switch (s) {
case ItemStatus.Init:
return Icons.hourglass_empty;
case ItemStatus.Progress:
return Icons.downloading;
case ItemStatus.Done:
return Icons.check_circle;
}
}
Color? _statusBg(ItemStatus s, BuildContext context) {
switch (s) {
case ItemStatus.Init:
return Colors.grey.shade200;
case ItemStatus.Progress:
return Colors.blue;
case ItemStatus.Done:
return Colors.green;
}
}
Widget _resolutionBadge(String? res, BuildContext context) {
if (res == null || res.isEmpty) return const SizedBox.shrink();
final m = RegExp(r"\d+").firstMatch(res);
final v = m != null ? int.tryParse(m.group(0)!) : null;
IconData? icon;
Color color = Theme.of(context).colorScheme.primary;
String label = res;
if (v != null) {
if (v >= 2000) {
icon = Icons.four_k;
color = Colors.deepOrange;
label = '${v}p';
} else if (v >= 1000) {
icon = Icons.hd;
color = Colors.blue;
label = '${v}p';
} else {
icon = Icons.sd_card;
color = Colors.grey;
label = '${v}p';
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface.withOpacity(0.9),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withOpacity(0.5)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) Icon(icon, size: 14, color: color),
if (icon != null) const SizedBox(width: 4),
Text(
label,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface),
),
],
),
);
}
}
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:multimediaFlutter/core/status.dart';
class StatusChip extends StatelessWidget {
final ItemStatus? selected;
final ValueChanged<ItemStatus?> onChanged;
const StatusChip(
{super.key, required this.selected, required this.onChanged});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('Alle'),
selected: selected == null,
onSelected: (_) => onChanged(null),
),
for (final s in ItemStatus.values)
FilterChip(
label: Text(s.name),
selected: selected == s,
onSelected: (_) => onChanged(s),
),
],
);
}
}