Files
MookNote/lib/pages/playlist/playlist_add_item_page.dart
DelLevin-Home 103afaa65e v0.2.9
2026-08-07 21:26:10 +08:00

276 lines
9.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../../providers/app_provider.dart';
import '../../models/data_models.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../widgets/animated_star_rating.dart';
class PlaylistAddItemPage extends StatefulWidget {
final Playlist playlist;
const PlaylistAddItemPage({super.key, required this.playlist});
@override
State<PlaylistAddItemPage> createState() => _PlaylistAddItemPageState();
}
class _PlaylistAddItemPageState extends State<PlaylistAddItemPage> {
final _searchController = TextEditingController();
List<String> _existingItemIds = [];
String _keyword = '';
bool _loading = true;
@override
void initState() {
super.initState();
_loadExisting();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadExisting() async {
final provider = context.read<AppProvider>();
final ids = await provider.getPlaylistItemIds(widget.playlist.id);
if (!mounted) return;
setState(() {
_existingItemIds = ids;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final provider = context.watch<AppProvider>();
final type = widget.playlist.type;
// 获取对应类型的条目列表
List<_SelectableItem> items;
if (type == 'movie') {
items = provider.movies
.where((m) => !m.isDeleted)
.map((m) => _SelectableItem(
id: m.id,
title: m.title,
coverPath: m.posterPath,
rating: m.rating,
subtitle: m.directors.isNotEmpty ? m.directors.take(2).join(' / ') : '',
))
.toList();
} else if (type == 'book') {
items = provider.books
.where((b) => !b.isDeleted)
.map((b) => _SelectableItem(
id: b.id,
title: b.title,
coverPath: b.coverPath,
rating: b.rating,
subtitle: b.authors.isNotEmpty ? b.authors.take(2).join(' / ') : '',
))
.toList();
} else {
items = provider.games
.where((g) => !g.isDeleted)
.map((g) => _SelectableItem(
id: g.id,
title: g.title,
coverPath: g.coverPath,
rating: g.rating,
subtitle: g.developer.isNotEmpty ? g.developer.take(2).join(' / ') : '',
))
.toList();
}
// 搜索过滤
if (_keyword.isNotEmpty) {
items = items.where((i) => i.title.toLowerCase().contains(_keyword.toLowerCase())).toList();
}
return Scaffold(
appBar: AppBar(
title: Text('添加${widget.playlist.typeLabel}'),
),
body: Column(
children: [
// 搜索框
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: TextField(
controller: _searchController,
style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration(
hintText: '搜索${widget.playlist.typeLabel}名称',
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3), fontSize: 15),
prefixIcon: Icon(Icons.search, color: colors.onSurface.withValues(alpha: 0.4), size: 22),
suffixIcon: _searchController.text.isNotEmpty
? GestureDetector(
onTap: () {
_searchController.clear();
setState(() => _keyword = '');
},
child: Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
),
child: Icon(Icons.close, color: colors.onSurface.withValues(alpha: 0.5), size: 16),
),
)
: null,
filled: true,
fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: colors.primary, width: 1.5)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
onChanged: (v) => setState(() => _keyword = v),
),
),
// 列表
Expanded(
child: _loading
? const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))
: items.isEmpty
? Center(
child: Text('没有找到${widget.playlist.typeLabel}',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3))),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
itemCount: items.length,
itemBuilder: (context, index) => _buildItem(context, items[index], provider),
),
),
],
),
);
}
Widget _buildItem(BuildContext context, _SelectableItem item, AppProvider provider) {
final colors = Theme.of(context).colorScheme;
final isAdded = _existingItemIds.contains(item.id);
final type = widget.playlist.type;
return GestureDetector(
onTap: () async {
if (isAdded) {
final playlistItems = await provider.getPlaylistItems(widget.playlist.id);
final match = playlistItems.firstWhere((pi) => pi.itemId == item.id);
await provider.removePlaylistItem(match.id, widget.playlist.id);
setState(() {
_existingItemIds.remove(item.id);
});
} else {
final playlistItem = PlaylistItem(
id: const Uuid().v4(),
playlistId: widget.playlist.id,
itemId: item.id,
addedAt: DateTime.now(),
);
await provider.addPlaylistItem(playlistItem);
setState(() {
_existingItemIds.add(item.id);
});
}
},
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isAdded ? colors.surfaceContainerHighest.withValues(alpha: 0.5) : colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isAdded ? colors.outlineVariant.withValues(alpha: 0.2) : colors.outlineVariant.withValues(alpha: 0.5),
width: 0.5,
),
),
child: Row(
children: [
// 封面
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
width: 40, height: 56,
child: item.coverPath != null && item.coverPath!.isNotEmpty
? FadeInLocalImage(
path: item.coverPath,
fit: BoxFit.cover,
errorWidget: _buildPlaceholder(type, colors),
)
: _buildPlaceholder(type, colors),
),
),
const SizedBox(width: 10),
// 信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isAdded ? colors.onSurface.withValues(alpha: 0.4) : colors.onSurface)),
if (item.subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(item.subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
],
],
),
),
const SizedBox(width: 8),
if (item.rating != null)
AnimatedStarRating(rating: item.rating!, starSize: 10, showNumber: true)
else if (!isAdded)
const SizedBox(width: 40),
if (isAdded) ...[
const SizedBox(width: 6),
Icon(Icons.check_circle, size: 18, color: colors.primary),
],
],
),
),
);
}
Widget _buildPlaceholder(String type, ColorScheme colors) {
final icon = switch (type) {
'movie' => Icons.movie_outlined,
'book' => Icons.menu_book_outlined,
'game' => Icons.sports_esports_outlined,
_ => Icons.list_outlined,
};
return Container(
color: colors.surfaceContainerHighest,
child: Center(child: Icon(icon, size: 18, color: colors.onSurface.withValues(alpha: 0.25))),
);
}
}
class _SelectableItem {
final String id;
final String title;
final String? coverPath;
final double? rating;
final String subtitle;
_SelectableItem({
required this.id,
required this.title,
this.coverPath,
this.rating,
this.subtitle = '',
});
}