diff --git a/assets/images/ticket/maoyan.png b/assets/images/ticket/maoyan.png new file mode 100644 index 0000000..a0585d8 Binary files /dev/null and b/assets/images/ticket/maoyan.png differ diff --git a/assets/images/ticket/qr.png b/assets/images/ticket/qr.png new file mode 100644 index 0000000..7ab0463 Binary files /dev/null and b/assets/images/ticket/qr.png differ diff --git a/lib/pages/movies/movie_share_page.dart b/lib/pages/movies/movie_share_page.dart index 88533e5..24e9af7 100644 --- a/lib/pages/movies/movie_share_page.dart +++ b/lib/pages/movies/movie_share_page.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -9,10 +8,8 @@ import '../../models/data_models.dart'; import '../../utils/toast_util.dart'; import '../../widgets/fade_in_local_image.dart'; -/// 影视分享海报页面 class MovieSharePage extends StatefulWidget { final Movie movie; - const MovieSharePage({super.key, required this.movie}); @override @@ -22,6 +19,72 @@ class MovieSharePage extends StatefulWidget { class _MovieSharePageState extends State { final GlobalKey _posterKey = GlobalKey(); bool _isGenerating = false; + int _currentStyle = 0; + + static const _styleNames = ['海报', '经典票根', '喵眼电影票根', '收藏版票根']; + + // 影院信息(可编辑) + late TextEditingController _cinemaCtrl; + late TextEditingController _hallCtrl; + late TextEditingController _seatRowCtrl; + late TextEditingController _seatNumCtrl; + late TextEditingController _screeningDateCtrl; + late TextEditingController _screeningTimeCtrl; + late TextEditingController _priceCtrl; + late TextEditingController _serviceFeeCtrl; + late TextEditingController _issueTimeCtrl; + late TextEditingController _platformCtrl; + late TextEditingController _ticketCodeCtrl; + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + final dateStr = '${now.year}年${now.month}月${now.day}日'; + final timeStr = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; + final issueStr = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; + final codeStr = 'MK${now.millisecondsSinceEpoch.toString().substring(1)}'; + + _cinemaCtrl = TextEditingController(text: '万达影城'); + _hallCtrl = TextEditingController(text: '3号厅'); + _seatRowCtrl = TextEditingController(text: '8'); + _seatNumCtrl = TextEditingController(text: '12'); + _screeningDateCtrl = TextEditingController(text: dateStr); + _screeningTimeCtrl = TextEditingController(text: timeStr); + _priceCtrl = TextEditingController(text: '39.90'); + _serviceFeeCtrl = TextEditingController(text: '5.00'); + _issueTimeCtrl = TextEditingController(text: issueStr); + _platformCtrl = TextEditingController(text: '喵眼电影'); + _ticketCodeCtrl = TextEditingController(text: codeStr); + } + + @override + void dispose() { + _cinemaCtrl.dispose(); + _hallCtrl.dispose(); + _seatRowCtrl.dispose(); + _seatNumCtrl.dispose(); + _screeningDateCtrl.dispose(); + _screeningTimeCtrl.dispose(); + _priceCtrl.dispose(); + _serviceFeeCtrl.dispose(); + _issueTimeCtrl.dispose(); + _platformCtrl.dispose(); + _ticketCodeCtrl.dispose(); + super.dispose(); + } + + bool get _isCinemaStyle => _currentStyle >= 2; + + Widget _buildBody() { + switch (_currentStyle) { + case 0: return _buildPosterWidget(); + case 1: return _buildTicketClassic(); + case 2: return _buildTicketCinema(); + case 3: return _buildTicketCollectible(); + default: return _buildPosterWidget(); + } + } @override Widget build(BuildContext context) { @@ -31,53 +94,232 @@ class _MovieSharePageState extends State { appBar: AppBar( backgroundColor: colors.surface, elevation: 0, - leading: IconButton( - icon: Icon(Icons.close, color: colors.onSurface), - onPressed: () => Navigator.pop(context), - ), - title: Text( - '分享海报', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: colors.onSurface, - ), - ), + leading: IconButton(icon: Icon(Icons.close, color: colors.onSurface), onPressed: () => Navigator.pop(context)), + title: Text('分享海报', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)), centerTitle: true, actions: [ + IconButton(icon: Icon(Icons.palette_outlined, color: colors.onSurface, size: 22), tooltip: '选择样式', onPressed: _showStylePicker), TextButton( onPressed: _isGenerating ? null : _generateAndShare, child: _isGenerating - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text( - '分享', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: colors.onSurface, - ), - ), + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) + : Text('分享', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), ), - const SizedBox(width: 8), + const SizedBox(width: 4), ], ), - body: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: RepaintBoundary( - key: _posterKey, - child: _buildPosterWidget(), + body: Column(children: [ + // 预览 + Expanded(child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: RepaintBoundary(key: _posterKey, child: _buildBody()), + ), + )), + // 底部编辑按钮(仅票根样式) + if (_isCinemaStyle) _buildBottomEditBar(colors), + ]), + ); + } + + // ─── 样式选择 ─── + + void _showStylePicker() { + final colors = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: colors.surface, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (ctx) => Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('选择样式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), + const SizedBox(height: 16), + Wrap(spacing: 10, runSpacing: 10, children: List.generate(_styleNames.length, (i) { + final selected = _currentStyle == i; + return GestureDetector( + onTap: () { setState(() => _currentStyle = i); Navigator.pop(ctx); }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), + decoration: BoxDecoration( + color: selected ? colors.primary : colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: selected ? colors.primary : colors.outline, width: 0.5), + ), + child: Text(_styleNames[i], style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, + color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6))), + ), + ); + })), + ]), + ), + ); + } + + // ─── 底部编辑栏 ─── + + Widget _buildBottomEditBar(ColorScheme colors) { + return Container( + padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.of(context).padding.bottom + 12, top: 8), + color: colors.surface, + child: SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton.icon( + onPressed: _showEditSheet, + icon: const Icon(Icons.edit_outlined, size: 18), + label: const Text('编辑票根信息'), + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, + foregroundColor: colors.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ), ), ); } - /// 构建海报 Widget + void _showEditSheet() { + final colors = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: colors.surface, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (ctx) => Padding( + padding: EdgeInsets.fromLTRB(20, 16, 20, MediaQuery.of(ctx).viewInsets.bottom + 20), + child: SingleChildScrollView(child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('编辑票根信息', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)), + const SizedBox(height: 16), + _sheetField('影院名称', _cinemaCtrl), + _sheetField('影厅', _hallCtrl), + Row(children: [ + Expanded(child: _sheetField('排', _seatRowCtrl)), + const SizedBox(width: 12), + Expanded(child: _sheetField('座', _seatNumCtrl)), + ]), + Row(children: [ + Expanded(child: _sheetField('票价 (¥)', _priceCtrl)), + const SizedBox(width: 12), + Expanded(child: _sheetField('服务费 (¥)', _serviceFeeCtrl)), + ]), + _sheetDatePicker('出票日期', _issueTimeCtrl, ctx), + _sheetCodeField(), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, height: 44, + child: ElevatedButton( + onPressed: () { setState(() {}); Navigator.pop(ctx); }, + style: ElevatedButton.styleFrom( + backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('完成'), + ), + ), + ], + )), + ), + ); + } + + Widget _sheetField(String label, TextEditingController ctrl) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: ctrl, + style: TextStyle(fontSize: 14, color: colors.onSurface), + decoration: InputDecoration( + labelText: label, + labelStyle: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + filled: true, fillColor: colors.surfaceContainerHighest, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)), + ), + ), + ); + } + + Widget _sheetDatePicker(String label, TextEditingController ctrl, BuildContext sheetCtx) { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: GestureDetector( + onTap: () async { + final now = DateTime.now(); + final picked = await showDatePicker( + context: sheetCtx, + initialDate: now, + firstDate: DateTime(1900), + lastDate: DateTime(now.year + 10), + locale: const Locale('zh'), + ); + if (picked != null) { + ctrl.text = '${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}'; + } + }, + child: AbsorbPointer(child: TextField( + controller: ctrl, + style: TextStyle(fontSize: 14, color: colors.onSurface), + decoration: InputDecoration( + labelText: label, + labelStyle: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + filled: true, fillColor: colors.surfaceContainerHighest, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)), + suffixIcon: Icon(Icons.calendar_today_outlined, size: 18, color: colors.onSurface.withValues(alpha: 0.4)), + ), + )), + ), + ); + } + + Widget _sheetCodeField() { + final colors = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row(children: [ + Expanded(child: TextField( + controller: _ticketCodeCtrl, + style: TextStyle(fontSize: 14, color: colors.onSurface, fontFamily: 'monospace'), + decoration: InputDecoration( + labelText: '编码', + labelStyle: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + filled: true, fillColor: colors.surfaceContainerHighest, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)), + ), + )), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + _ticketCodeCtrl.text = 'MK${DateTime.now().millisecondsSinceEpoch.toString().substring(1)}'; + }, + child: Container( + width: 40, height: 40, + decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)), + child: Icon(Icons.casino_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), + ), + ), + ]), + ); + } + + // ═══════════════════════════════════════════════════════════ + // 样式 0:海报 + // ═══════════════════════════════════════════════════════════ + Widget _buildPosterWidget() { final colors = Theme.of(context).colorScheme; final movie = widget.movie; @@ -85,240 +327,389 @@ class _MovieSharePageState extends State { return Container( width: 320, - decoration: BoxDecoration( - color: colors.surface, - borderRadius: BorderRadius.circular(16), + decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))]), + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (hasPoster) + ClipRRect(borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + child: FadeInLocalImage(path: movie.posterPath, width: 320, height: 200, fit: BoxFit.cover)), + Padding(padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(movie.title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: colors.onSurface)), + if (movie.alternateTitles.isNotEmpty) ...[ + const SizedBox(height: 8), + Text(movie.alternateTitles.join(' / '), style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))), + ], + const SizedBox(height: 16), + if (movie.rating != null && movie.rating! > 0) ...[ + Row(children: [ + const Icon(Icons.star, size: 18, color: Color(0xFFFFB800)), + const SizedBox(width: 4), + Text(movie.rating!.toStringAsFixed(1), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFFFB800))), + const SizedBox(width: 4), + Text('/ 10', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + ]), + const SizedBox(height: 12), + ], + if (movie.directors.isNotEmpty) _infoRow('导演', movie.directors.join(' / '), colors), + if (movie.writers.isNotEmpty) _infoRow('编剧', movie.writers.join(' / '), colors), + if (movie.actors.isNotEmpty) _infoRow('主演', movie.actors.take(3).join(' / '), colors), + if (movie.genres.isNotEmpty) _infoRow('类型', movie.genres.join(' / '), colors), + if (movie.releaseDate != null) _infoRow('上映', _fmtDate(movie.releaseDate!), colors), + if (movie.summary != null && movie.summary!.isNotEmpty) ...[ + const SizedBox(height: 16), + Text('简介', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), + const SizedBox(height: 8), + Text(movie.summary!, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6), maxLines: 5, overflow: TextOverflow.ellipsis), + ], + const SizedBox(height: 20), + Divider(height: 1, color: colors.outline), + const SizedBox(height: 12), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.movie_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.5)), + const SizedBox(width: 6), + Text('来自 MookNote', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))), + ]), + ])), + ]), + ); + } + + Widget _infoRow(String label, String value, ColorScheme colors) { + return Padding(padding: const EdgeInsets.only(bottom: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('$label:', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))), + Expanded(child: Text(value, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75)))), + ])); + } + + // ═══════════════════════════════════════════════════════════ + // 样式 1:经典票根 + // ═══════════════════════════════════════════════════════════ + + Widget _buildTicketClassic() { + final movie = widget.movie; + final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty; + const c = Color(0xFF2D2D2D); + + return Container( + width: 300, + decoration: BoxDecoration(color: const Color(0xFFFFFBF5), borderRadius: BorderRadius.circular(8), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 16, offset: const Offset(0, 6))]), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Padding(padding: const EdgeInsets.all(16), child: Column(children: [ + if (hasPoster) ClipRRect(borderRadius: BorderRadius.circular(4), + child: FadeInLocalImage(path: movie.posterPath, width: 268, height: 160, fit: BoxFit.cover)), + const SizedBox(height: 12), + Text(movie.title, textAlign: TextAlign.center, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: c, letterSpacing: 1)), + if (movie.alternateTitles.isNotEmpty) ...[ + const SizedBox(height: 4), + Text(movie.alternateTitles.join(' / '), textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: c.withValues(alpha: 0.4))), + ], + ])), + _dashedLine(c.withValues(alpha: 0.15)), + Padding(padding: const EdgeInsets.fromLTRB(20, 14, 20, 16), child: Column(children: [ + _classicRow('DIRECTED BY', movie.directors.isNotEmpty ? movie.directors.join(', ') : '--'), + const SizedBox(height: 10), + _classicRow('STARRING', movie.actors.isNotEmpty ? movie.actors.take(3).join(', ') : '--'), + const SizedBox(height: 10), + _classicRow('GENRE', movie.genres.isNotEmpty ? movie.genres.join(' / ') : '--'), + const SizedBox(height: 10), + _classicRow('RELEASE', movie.releaseDate != null ? _fmtDate(movie.releaseDate!) : '--'), + if (movie.rating != null && movie.rating! > 0) ...[ + const SizedBox(height: 10), + _classicRow('RATING', '${movie.rating!.toStringAsFixed(1)} / 10'), + ], + const SizedBox(height: 14), + Row(children: [ + Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration(border: Border.all(color: c.withValues(alpha: 0.2), width: 0.5)), + child: Text(_statusEN(movie.status), style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, letterSpacing: 2, color: c.withValues(alpha: 0.5)))), + const Spacer(), + Icon(Icons.movie_outlined, size: 12, color: c.withValues(alpha: 0.3)), + const SizedBox(width: 4), + Text('MookNote', style: TextStyle(fontSize: 9, letterSpacing: 1, color: c.withValues(alpha: 0.3))), + ]), + ])), + ]), + ); + } + + Widget _classicRow(String label, String value) { + return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(width: 80, child: Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, letterSpacing: 1.5, color: const Color(0xFF2D2D2D).withValues(alpha: 0.35)))), + Expanded(child: Text(value, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF2D2D2D), height: 1.4))), + ]); + } + + // ═══════════════════════════════════════════════════════════ + // 样式 2:电影票(参考票根.html) + // ═══════════════════════════════════════════════════════════ + + Widget _buildTicketCinema() { + final movie = widget.movie; + const bg = Color(0xFFFFFDF7); + const text = Color(0xFF1A1A1A); + const muted = Color(0xFF999999); + + return Container( + width: 340, + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 24, offset: const Offset(0, 6))]), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + // Header + Padding(padding: const EdgeInsets.fromLTRB(20, 16, 20, 14), child: Row(children: [ + Image.asset('assets/images/ticket/maoyan.png', width: 36, height: 36, errorBuilder: (_, __, ___) => Icon(Icons.local_movies, size: 36, color: text)), + const SizedBox(width: 10), + Text(_platformCtrl.text.isNotEmpty ? _platformCtrl.text : 'MookNote', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: text, letterSpacing: 0.5)), + ])), + Container(height: 0.5, color: const Color(0xFFF0EDE5)), + + // Body + Padding(padding: const EdgeInsets.fromLTRB(20, 18, 20, 14), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 影院 + Text(_cinemaCtrl.text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: text, letterSpacing: 0.5)), + const SizedBox(height: 14), + // 片名 + Text(movie.title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: text, letterSpacing: 0.5, height: 1.3)), + const SizedBox(height: 4), + Text(_subtitleText(movie), style: const TextStyle(fontSize: 12, color: Color(0xFF666666), letterSpacing: 0.5)), + const SizedBox(height: 16), + // 场次 + Row(children: [ + Icon(Icons.desktop_windows_outlined, size: 15, color: text.withValues(alpha: 0.6)), + const SizedBox(width: 8), + Text(_hallCtrl.text, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: text.withValues(alpha: 0.8))), + ]), + const SizedBox(height: 8), + Row(children: [ + Text('${_seatRowCtrl.text}排${_seatNumCtrl.text}座', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: text.withValues(alpha: 0.8))), + Padding(padding: const EdgeInsets.symmetric(horizontal: 8), child: Text('|', style: TextStyle(color: const Color(0xFFD0CCC0), fontWeight: FontWeight.w300))), + Text(widget.movie.releaseDate != null ? '${widget.movie.releaseDate!.year}年${widget.movie.releaseDate!.month}月${widget.movie.releaseDate!.day}日' : '待定', style: TextStyle(fontSize: 14, color: text.withValues(alpha: 0.6))), + ]), + ])), + + // 撕票线 + _tearScissors(bg), + + // Footer + Padding(padding: const EdgeInsets.fromLTRB(20, 10, 20, 16), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Container(width: 100, height: 100, decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE8E4DA), width: 0.5)), + child: Image.asset('assets/images/ticket/qr.png', fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox.shrink())), + const SizedBox(width: 16), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _ticketDetail('票价', _priceCtrl.text, isPrice: true), + _ticketDetail('服务费', _serviceFeeCtrl.text), + _ticketDetail('出票日期', _issueTimeCtrl.text), + _ticketDetail(_platformCtrl.text.isNotEmpty ? _platformCtrl.text : '平台', '已出票'), + const SizedBox(height: 4), + Text(_ticketCodeCtrl.text, style: TextStyle(fontFamily: 'monospace', fontSize: 9, letterSpacing: 0.8, color: muted)), + ])), + ])), + ]), + ); + } + + Widget _ticketDetail(String label, String value, {bool isPrice = false}) { + return Padding(padding: const EdgeInsets.only(bottom: 4), child: Row(children: [ + Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF999999))), + const Spacer(), + Text(value, style: TextStyle( + fontSize: isPrice ? 16 : 12, + fontWeight: isPrice ? FontWeight.w700 : FontWeight.w500, + color: isPrice ? const Color(0xFFE5342F) : const Color(0xFF1A1A1A), + )), + ])); + } + + // ═══════════════════════════════════════════════════════════ + // 样式 3:收藏版票根(参考票根收藏版.html) + // ═══════════════════════════════════════════════════════════ + + Widget _buildTicketCollectible() { + final movie = widget.movie; + const red = Color(0xFFC0392B); + const text = Color(0xFF1A1A1A); + const light = Color(0xFF8C8C8C); + const line = Color(0xFFE8E2D8); + + return Container( + width: 360, + decoration: BoxDecoration(color: const Color(0xFFFFFFFB), boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 20, - offset: const Offset(0, 10), + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 2)), + BoxShadow(color: Colors.black.withValues(alpha: 0.10), blurRadius: 40, offset: const Offset(0, 8)), + ]), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + // 红色头部 + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + color: red, + child: Row(children: [ + const Text('MookNote', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: 0.5)), + const Spacer(), + Text('收藏纪念\nCOLLECTIBLE', textAlign: TextAlign.right, + style: TextStyle(fontSize: 8, color: Colors.white.withValues(alpha: 0.7), letterSpacing: 0.5, height: 1.5)), + ]), + ), + + // 主体 + Padding(padding: const EdgeInsets.fromLTRB(24, 22, 24, 0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 影院 + Row(children: [ + Icon(Icons.location_on_outlined, size: 14, color: red), + const SizedBox(width: 4), + Text(_cinemaCtrl.text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF6B6B6B), letterSpacing: 0.3)), + ]), + const SizedBox(height: 16), + // 标题 + Text(movie.title, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: text, letterSpacing: 0.5, height: 1.15)), + const SizedBox(height: 6), + // 标签 + Wrap(spacing: 8, runSpacing: 6, children: [ + if (movie.genres.isNotEmpty) ...movie.genres.take(3).map((g) => _tagChip(g)), + if (movie.releaseDate != null) _tagChip(_fmtDate(movie.releaseDate!)), + ]), + const SizedBox(height: 20), + // 三列网格 + Container( + decoration: BoxDecoration(border: Border(top: BorderSide(color: line, width: 0.5), bottom: BorderSide(color: line, width: 0.5))), + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row(children: [ + _gridCol('影厅', _hallCtrl.text, red), + Container(width: 0.5, height: 36, color: const Color(0xFFF2EDE5)), + _gridCol('座位', '${_seatRowCtrl.text}排${_seatNumCtrl.text}座', text), + Container(width: 0.5, height: 36, color: const Color(0xFFF2EDE5)), + _gridCol('上映', movie.releaseDate != null ? _fmtDate(movie.releaseDate!) : '--', red), + ]), ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 海报图片 - if (hasPoster) - ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - child: FadeInLocalImage( - path: movie.posterPath, - width: 320, - height: 200, - fit: BoxFit.cover, - ), - ), + ])), - // 内容区域 - Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 - Text( - movie.title, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: colors.onSurface, - ), - ), + // 撕票线(圆形缺口) + _tearCircles(), - // 别名 - if (movie.alternateTitles.isNotEmpty) ...[ - const SizedBox(height: 8), - Text( - movie.alternateTitles.join(' / '), - style: TextStyle( - fontSize: 14, - color: colors.onSurface.withValues(alpha: 0.6), - ), - ), - ], + // 底部信息 + Padding(padding: const EdgeInsets.fromLTRB(24, 10, 24, 0), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Container(width: 120, height: 120, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFE8E2D8), width: 0.5)), + child: Image.asset('assets/images/ticket/qr.png', fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox.shrink())), + const SizedBox(width: 16), + Expanded(child: Column(children: [ + _collectInfo('票价', '¥${_priceCtrl.text}', isPrice: true), + _collectInfo('服务费', '¥${_serviceFeeCtrl.text}'), + _collectInfo('时间', _issueTimeCtrl.text), + _collectInfo('平台', 'MookNote'), + _collectInfo('编码', _ticketCodeCtrl.text, isCode: true), + ])), + ])), - const SizedBox(height: 16), - - // 评分 - if (movie.rating != null && movie.rating! > 0) ...[ - Row( - children: [ - const Icon( - Icons.star, - size: 18, - color: Color(0xFFFFB800), - ), - const SizedBox(width: 4), - Text( - movie.rating!.toStringAsFixed(1), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color(0xFFFFB800), - ), - ), - const SizedBox(width: 4), - Text( - '/ 10', - style: TextStyle( - fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.4), - ), - ), - ], - ), - const SizedBox(height: 12), - ], - - // 导演 - if (movie.directors.isNotEmpty) - _buildInfoRow('导演', movie.directors.join(' / '), colors), - - // 编剧 - if (movie.writers.isNotEmpty) - _buildInfoRow('编剧', movie.writers.join(' / '), colors), - - // 主演 - if (movie.actors.isNotEmpty) - _buildInfoRow('主演', movie.actors.take(3).join(' / '), colors), - - // 类型 - if (movie.genres.isNotEmpty) - _buildInfoRow('类型', movie.genres.join(' / '), colors), - - // 上映日期 - if (movie.releaseDate != null) - _buildInfoRow( - '上映', - '${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}', - colors, - ), - - const SizedBox(height: 16), - - // 简介 - if (movie.summary != null && movie.summary!.isNotEmpty) ...[ - Text( - '简介', - style: TextStyle( - fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.4), - ), - ), - const SizedBox(height: 8), - Text( - movie.summary!, - style: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.6), - height: 1.6, - ), - maxLines: 5, - overflow: TextOverflow.ellipsis, - ), - ], - - const SizedBox(height: 20), - - // 底部标识 - Divider(height: 1, color: colors.outline), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.movie_outlined, - size: 14, - color: colors.onSurface.withValues(alpha: 0.5), - ), - const SizedBox(width: 6), - Text( - '来自 MookNote', - style: TextStyle( - fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.5), - ), - ), - ], - ), - ], - ), - ), - ], - ), + // 底部条 + Padding(padding: const EdgeInsets.fromLTRB(24, 12, 24, 14), child: Row(children: [ + Text('MOOKNOTE MOVIE · TICKET', style: TextStyle(fontSize: 9, letterSpacing: 1, color: light.withValues(alpha: 0.7))), + const Spacer(), + Text(_ticketCodeCtrl.text, style: TextStyle(fontSize: 9, letterSpacing: 0.5, color: light.withValues(alpha: 0.7))), + ])), + ]), ); } - /// 构建信息行 - Widget _buildInfoRow(String label, String value, ColorScheme colors) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '$label:', - style: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.4), - ), - ), - Expanded( - child: Text( - value, - style: TextStyle( - fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.75), - ), - ), - ), - ], - ), - ); + Widget _tagChip(String text) { + return Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration(border: Border.all(color: const Color(0xFFEBE5D9), width: 0.5), borderRadius: BorderRadius.circular(3)), + child: Text(text, style: const TextStyle(fontSize: 10, color: Color(0xFF6B6B6B), letterSpacing: 0.5))); } - /// 生成并分享海报 + Widget _gridCol(String label, String value, Color valueColor) { + return Expanded(child: Column(children: [ + Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFF8C8C8C), letterSpacing: 1)), + const SizedBox(height: 4), + Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: valueColor, letterSpacing: 0.5)), + ])); + } + + Widget _collectInfo(String label, String value, {bool isPrice = false, bool isCode = false}) { + return Padding(padding: const EdgeInsets.only(bottom: 3), child: Row(children: [ + SizedBox(width: 32, child: Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFF8C8C8C), letterSpacing: 0.5))), + Expanded(child: Text(value, textAlign: TextAlign.right, style: TextStyle( + fontSize: isPrice ? 15 : 10, + fontWeight: isPrice ? FontWeight.w700 : FontWeight.w500, + color: isPrice ? const Color(0xFFC0392B) : const Color(0xFF1A1A1A).withValues(alpha: 0.8), + fontFamily: isCode ? 'monospace' : null, + letterSpacing: isCode ? 0.5 : 0, + ))), + ])); + } + + // ─── 通用组件 ─── + + Widget _dashedLine(Color color) { + return Padding(padding: const EdgeInsets.symmetric(horizontal: 8), + child: CustomPaint(size: const Size(double.infinity, 1), painter: _DashedLinePainter(color: color))); + } + + Widget _tearScissors(Color bg) { + return SizedBox(height: 28, child: Stack(children: [ + Positioned(left: 20, right: 20, top: 13, child: CustomPaint( + size: const Size(double.infinity, 1), painter: _DashedLinePainter(color: const Color(0xFFC8C4B8), dashWidth: 6, dashSpace: 8))), + Positioned(left: 2, top: 4, child: Icon(Icons.content_cut, size: 16, color: const Color(0xFF999999))), + ])); + } + + Widget _tearCircles() { + return Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: SizedBox(height: 24, child: Stack(children: [ + Positioned(left: 0, right: 0, top: 11, child: CustomPaint( + size: const Size(double.infinity, 1), painter: _DashedLinePainter(color: const Color(0xFFD8D0C0), dashWidth: 9, dashSpace: 9))), + Positioned(left: -30, top: 5.5, child: Container(width: 13, height: 13, decoration: BoxDecoration(color: const Color(0xFFEBE5DB), shape: BoxShape.circle))), + Positioned(right: -30, top: 5.5, child: Container(width: 13, height: 13, decoration: BoxDecoration(color: const Color(0xFFEBE5DB), shape: BoxShape.circle))), + ]))); + } + + String _fmtDate(DateTime d) => '${d.year}.${d.month.toString().padLeft(2, '0')}.${d.day.toString().padLeft(2, '0')}'; + + String _subtitleText(Movie m) { + final parts = []; + if (m.genres.isNotEmpty) parts.add(m.genres.join(' / ')); + return parts.join(' · '); + } + + String _statusEN(String s) { + switch (s) { case 'watched': return 'ADMITTED'; case 'watching': return 'NOW PLAYING'; default: return 'RESERVED'; } + } + + // ─── 生成分享 ─── + Future _generateAndShare() async { setState(() => _isGenerating = true); - try { - // 捕获海报图片 - final boundary = _posterKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; - if (boundary == null) { - throw Exception('无法获取海报边界'); - } - + final boundary = _posterKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + if (boundary == null) throw Exception('无法获取海报边界'); final image = await boundary.toImage(pixelRatio: 3.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); - if (byteData == null) { - throw Exception('无法生成图片数据'); - } - - final bytes = byteData.buffer.asUint8List(); - - // 保存到临时文件 + if (byteData == null) throw Exception('无法生成图片数据'); final tempDir = await getTemporaryDirectory(); - final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.png'; - final file = File('${tempDir.path}/$fileName'); - await file.writeAsBytes(bytes); - - // 分享 - await Share.shareXFiles( - [XFile(file.path)], - text: '分享影视:${widget.movie.title}', - ); + final file = File('${tempDir.path}/movie_poster_${DateTime.now().millisecondsSinceEpoch}.png'); + await file.writeAsBytes(byteData.buffer.asUint8List()); + await Share.shareXFiles([XFile(file.path)], text: '分享影视:${widget.movie.title}'); } catch (e) { - if (mounted) { - ToastUtil.show(context, '生成海报失败:$e'); - } + if (mounted) ToastUtil.show(context, '生成海报失败:$e'); } finally { - if (mounted) { - setState(() => _isGenerating = false); - } + if (mounted) setState(() => _isGenerating = false); } } } + +class _DashedLinePainter extends CustomPainter { + final Color color; + final double dashWidth; + final double dashSpace; + _DashedLinePainter({required this.color, this.dashWidth = 4, this.dashSpace = 4}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color..strokeWidth = 1..style = PaintingStyle.stroke; + double x = 0; + while (x < size.width) { canvas.drawLine(Offset(x, 0), Offset(x + dashWidth, 0), paint); x += dashWidth + dashSpace; } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/widgets/book_status_bar.dart b/lib/widgets/book_status_bar.dart index a8749a8..93b7f1c 100644 --- a/lib/widgets/book_status_bar.dart +++ b/lib/widgets/book_status_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; -/// 阅读状态选择栏 - 水滴滑动动画 +/// 阅读状态选择栏 - 交叉渐变动画 class BookStatusBar extends StatelessWidget { const BookStatusBar({super.key}); @@ -15,9 +15,7 @@ class BookStatusBar extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: colors.surface, - border: Border( - bottom: BorderSide(color: colors.outline, width: 0.5), - ), + border: Border(bottom: BorderSide(color: colors.outline, width: 0.5)), ), child: Container( padding: const EdgeInsets.all(4), @@ -25,66 +23,13 @@ class BookStatusBar extends StatelessWidget { color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(24), ), - child: LayoutBuilder( - builder: (context, constraints) { - final tabWidth = constraints.maxWidth / 3; - return SizedBox( - height: 40, - child: Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 800), - curve: Curves.elasticOut, - left: provider.bookStatusIndex * tabWidth, - top: 0, - bottom: 0, - width: tabWidth, - child: Padding( - padding: const EdgeInsets.all(3), - child: Container( - decoration: BoxDecoration( - color: colors.primary, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - ), - ), - ), - Row( - children: [ - _buildTab( - colors: colors, - label: '已读', - icon: Icons.check_circle_outline, - isSelected: provider.bookStatusIndex == 0, - onTap: () => provider.setBookStatusIndex(0), - ), - _buildTab( - colors: colors, - label: '在读', - icon: Icons.menu_book_outlined, - isSelected: provider.bookStatusIndex == 1, - onTap: () => provider.setBookStatusIndex(1), - ), - _buildTab( - colors: colors, - label: '想读', - icon: Icons.bookmark_outlined, - isSelected: provider.bookStatusIndex == 2, - onTap: () => provider.setBookStatusIndex(2), - ), - ], - ), - ], - ), - ); - }, + child: SizedBox( + height: 40, + child: Row(children: [ + _buildTab(colors, '已读', Icons.check_circle_outline, provider.bookStatusIndex == 0, () => provider.setBookStatusIndex(0)), + _buildTab(colors, '在读', Icons.menu_book_outlined, provider.bookStatusIndex == 1, () => provider.setBookStatusIndex(1)), + _buildTab(colors, '想读', Icons.bookmark_outlined, provider.bookStatusIndex == 2, () => provider.setBookStatusIndex(2)), + ]), ), ), ); @@ -92,39 +37,45 @@ class BookStatusBar extends StatelessWidget { ); } - Widget _buildTab({ - required ColorScheme colors, - required String label, - required IconData icon, - required bool isSelected, - required VoidCallback onTap, - }) { + Widget _buildTab(ColorScheme colors, String label, IconData icon, bool isSelected, VoidCallback onTap) { return Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, - child: Container( - height: double.infinity, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + margin: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: isSelected ? colors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(20), + boxShadow: isSelected ? [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2))] : null, + ), alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 16, - color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 6), - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + style: TextStyle( + fontSize: 13, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: Icon(icon, key: ValueKey(isSelected), size: 16, + color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6)), ), - ), - ], + const SizedBox(width: 6), + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: Text(label, key: ValueKey('$label$isSelected')), + ), + ], + ), ), ), ), diff --git a/lib/widgets/movie_status_bar.dart b/lib/widgets/movie_status_bar.dart index 55c9573..ec36434 100644 --- a/lib/widgets/movie_status_bar.dart +++ b/lib/widgets/movie_status_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; -/// 观影状态选择栏 - 水滴滑动动画 +/// 观影状态选择栏 - 交叉渐变动画 class MovieStatusBar extends StatelessWidget { const MovieStatusBar({super.key}); @@ -15,9 +15,7 @@ class MovieStatusBar extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: colors.surface, - border: Border( - bottom: BorderSide(color: colors.outline, width: 0.5), - ), + border: Border(bottom: BorderSide(color: colors.outline, width: 0.5)), ), child: Container( padding: const EdgeInsets.all(4), @@ -25,66 +23,13 @@ class MovieStatusBar extends StatelessWidget { color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(24), ), - child: LayoutBuilder( - builder: (context, constraints) { - final tabWidth = constraints.maxWidth / 3; - return SizedBox( - height: 40, - child: Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 800), - curve: Curves.elasticOut, - left: provider.movieStatusIndex * tabWidth, - top: 0, - bottom: 0, - width: tabWidth, - child: Padding( - padding: const EdgeInsets.all(3), - child: Container( - decoration: BoxDecoration( - color: colors.primary, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - ), - ), - ), - Row( - children: [ - _buildTab( - colors: colors, - label: '已看', - icon: Icons.check_circle_outline, - isSelected: provider.movieStatusIndex == 0, - onTap: () => provider.setMovieStatusIndex(0), - ), - _buildTab( - colors: colors, - label: '在看', - icon: Icons.play_circle_outline, - isSelected: provider.movieStatusIndex == 1, - onTap: () => provider.setMovieStatusIndex(1), - ), - _buildTab( - colors: colors, - label: '想看', - icon: Icons.bookmark_outline, - isSelected: provider.movieStatusIndex == 2, - onTap: () => provider.setMovieStatusIndex(2), - ), - ], - ), - ], - ), - ); - }, + child: SizedBox( + height: 40, + child: Row(children: [ + _buildTab(colors, '已看', Icons.check_circle_outline, provider.movieStatusIndex == 0, () => provider.setMovieStatusIndex(0)), + _buildTab(colors, '在看', Icons.play_circle_outline, provider.movieStatusIndex == 1, () => provider.setMovieStatusIndex(1)), + _buildTab(colors, '想看', Icons.bookmark_outline, provider.movieStatusIndex == 2, () => provider.setMovieStatusIndex(2)), + ]), ), ), ); @@ -92,39 +37,45 @@ class MovieStatusBar extends StatelessWidget { ); } - Widget _buildTab({ - required ColorScheme colors, - required String label, - required IconData icon, - required bool isSelected, - required VoidCallback onTap, - }) { + Widget _buildTab(ColorScheme colors, String label, IconData icon, bool isSelected, VoidCallback onTap) { return Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, - child: Container( - height: double.infinity, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + margin: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: isSelected ? colors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(20), + boxShadow: isSelected ? [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2))] : null, + ), alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 16, - color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 6), - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + style: TextStyle( + fontSize: 13, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: Icon(icon, key: ValueKey(isSelected), size: 16, + color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6)), ), - ), - ], + const SizedBox(width: 6), + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: Text(label, key: ValueKey('$label$isSelected')), + ), + ], + ), ), ), ), diff --git a/pubspec.lock b/pubspec.lock index 8641285..2bed419 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.6.1" args: @@ -14,7 +14,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" async: @@ -22,7 +22,7 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.1" boolean_selector: @@ -30,7 +30,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" characters: @@ -38,7 +38,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" checked_yaml: @@ -46,7 +46,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" cli_util: @@ -54,7 +54,7 @@ packages: description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2" clock: @@ -62,7 +62,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" code_assets: @@ -70,7 +70,7 @@ packages: description: name: code_assets sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" collection: @@ -78,7 +78,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" cross_file: @@ -86,7 +86,7 @@ packages: description: name: cross_file sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.5+2" crypto: @@ -94,7 +94,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.7" cupertino_icons: @@ -102,7 +102,7 @@ packages: description: name: cupertino_icons sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.9" equatable: @@ -110,7 +110,7 @@ packages: description: name: equatable sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.8" extended_text_field: @@ -118,7 +118,7 @@ packages: description: name: extended_text_field sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "16.0.2" extended_text_library: @@ -126,7 +126,7 @@ packages: description: name: extended_text_library sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.0.1" fake_async: @@ -134,7 +134,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" ffi: @@ -142,7 +142,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" file: @@ -150,7 +150,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" file_picker: @@ -158,7 +158,7 @@ packages: description: name: file_picker sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.7" file_selector_linux: @@ -166,7 +166,7 @@ packages: description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.4" file_selector_macos: @@ -174,7 +174,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -182,7 +182,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" file_selector_windows: @@ -190,7 +190,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+5" fixnum: @@ -198,7 +198,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" fl_chart: @@ -206,7 +206,7 @@ packages: description: name: fl_chart sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.69.2" flutter: @@ -219,7 +219,7 @@ packages: description: name: flutter_launcher_icons sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.1" flutter_lints: @@ -227,7 +227,7 @@ packages: description: name: flutter_lints sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" flutter_localizations: @@ -240,7 +240,7 @@ packages: description: name: flutter_markdown_plus sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.7" flutter_plugin_android_lifecycle: @@ -248,7 +248,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.35" flutter_staggered_grid_view: @@ -256,7 +256,7 @@ packages: description: name: flutter_staggered_grid_view sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_test: @@ -274,7 +274,7 @@ packages: description: name: hooks sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" http: @@ -282,7 +282,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" http_parser: @@ -290,7 +290,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" image: @@ -298,7 +298,7 @@ packages: description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" image_picker: @@ -306,7 +306,7 @@ packages: description: name: image_picker sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" image_picker_android: @@ -314,7 +314,7 @@ packages: description: name: image_picker_android sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+17" image_picker_for_web: @@ -322,7 +322,7 @@ packages: description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" image_picker_ios: @@ -330,7 +330,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+6" image_picker_linux: @@ -338,7 +338,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" image_picker_macos: @@ -346,7 +346,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -354,7 +354,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.1" image_picker_windows: @@ -362,7 +362,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" intl: @@ -370,7 +370,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" jni: @@ -378,7 +378,7 @@ packages: description: name: jni sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" jni_flutter: @@ -386,7 +386,7 @@ packages: description: name: jni_flutter sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" json_annotation: @@ -394,7 +394,7 @@ packages: description: name: json_annotation sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.12.0" leak_tracker: @@ -402,7 +402,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -410,7 +410,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -418,7 +418,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -426,7 +426,7 @@ packages: description: name: lints sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" logging: @@ -434,7 +434,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" markdown: @@ -442,7 +442,7 @@ packages: description: name: markdown sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.3.1" matcher: @@ -450,7 +450,7 @@ packages: description: name: matcher sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.19" material_color_utilities: @@ -458,7 +458,7 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.0" meta: @@ -466,7 +466,7 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.17.0" mime: @@ -474,7 +474,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" nested: @@ -482,7 +482,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" objective_c: @@ -490,7 +490,7 @@ packages: description: name: objective_c sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.4.1" package_config: @@ -498,7 +498,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" package_info_plus: @@ -506,7 +506,7 @@ packages: description: name: package_info_plus sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.1" package_info_plus_platform_interface: @@ -514,7 +514,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" path: @@ -522,7 +522,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_provider: @@ -530,7 +530,7 @@ packages: description: name: path_provider sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.6" path_provider_android: @@ -538,7 +538,7 @@ packages: description: name: path_provider_android sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" path_provider_foundation: @@ -546,7 +546,7 @@ packages: description: name: path_provider_foundation sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.0" path_provider_linux: @@ -554,7 +554,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -562,7 +562,7 @@ packages: description: name: path_provider_platform_interface sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" path_provider_windows: @@ -570,7 +570,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" permission_handler: @@ -578,7 +578,7 @@ packages: description: name: permission_handler sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.4.0" permission_handler_android: @@ -586,7 +586,7 @@ packages: description: name: permission_handler_android sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.1.0" permission_handler_apple: @@ -594,7 +594,7 @@ packages: description: name: permission_handler_apple sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.4.10" permission_handler_html: @@ -602,7 +602,7 @@ packages: description: name: permission_handler_html sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.3+5" permission_handler_platform_interface: @@ -610,7 +610,7 @@ packages: description: name: permission_handler_platform_interface sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" permission_handler_windows: @@ -618,7 +618,7 @@ packages: description: name: permission_handler_windows sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.1" petitparser: @@ -626,7 +626,7 @@ packages: description: name: petitparser sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.2" platform: @@ -634,7 +634,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -642,7 +642,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" provider: @@ -650,7 +650,7 @@ packages: description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5+1" pub_semver: @@ -658,7 +658,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" record_use: @@ -666,7 +666,7 @@ packages: description: name: record_use sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" share_plus: @@ -674,7 +674,7 @@ packages: description: name: share_plus sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.1.4" share_plus_platform_interface: @@ -682,7 +682,7 @@ packages: description: name: share_plus_platform_interface sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.2" shared_preferences: @@ -690,7 +690,7 @@ packages: description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.5" shared_preferences_android: @@ -698,7 +698,7 @@ packages: description: name: shared_preferences_android sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.23" shared_preferences_foundation: @@ -706,7 +706,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.6" shared_preferences_linux: @@ -714,7 +714,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -722,7 +722,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" shared_preferences_web: @@ -730,7 +730,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" shared_preferences_windows: @@ -738,7 +738,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" sky_engine: @@ -751,7 +751,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.2" sqflite: @@ -759,7 +759,7 @@ packages: description: name: sqflite sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2+1" sqflite_android: @@ -767,7 +767,7 @@ packages: description: name: sqflite_android sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2+3" sqflite_common: @@ -775,7 +775,7 @@ packages: description: name: sqflite_common sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.8" sqflite_darwin: @@ -783,7 +783,7 @@ packages: description: name: sqflite_darwin sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" sqflite_platform_interface: @@ -791,7 +791,7 @@ packages: description: name: sqflite_platform_interface sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" stack_trace: @@ -799,7 +799,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -807,7 +807,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" string_scanner: @@ -815,7 +815,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" synchronized: @@ -823,7 +823,7 @@ packages: description: name: synchronized sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.0+1" term_glyph: @@ -831,7 +831,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: @@ -839,7 +839,7 @@ packages: description: name: test_api sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.10" typed_data: @@ -847,7 +847,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" url_launcher: @@ -855,7 +855,7 @@ packages: description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.2" url_launcher_android: @@ -863,7 +863,7 @@ packages: description: name: url_launcher_android sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.30" url_launcher_ios: @@ -871,7 +871,7 @@ packages: description: name: url_launcher_ios sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" url_launcher_linux: @@ -879,7 +879,7 @@ packages: description: name: url_launcher_linux sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" url_launcher_macos: @@ -887,7 +887,7 @@ packages: description: name: url_launcher_macos sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.5" url_launcher_platform_interface: @@ -895,7 +895,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" url_launcher_web: @@ -903,7 +903,7 @@ packages: description: name: url_launcher_web sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" url_launcher_windows: @@ -911,7 +911,7 @@ packages: description: name: url_launcher_windows sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.5" uuid: @@ -919,7 +919,7 @@ packages: description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.3" vector_math: @@ -927,7 +927,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" vm_service: @@ -935,7 +935,7 @@ packages: description: name: vm_service sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.2.0" web: @@ -943,7 +943,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" webview_flutter: @@ -951,7 +951,7 @@ packages: description: name: webview_flutter sha256: e4d3474277c5043f5f3100ed27d94ad693abfd5c602b0221c719a4497e4ba1e4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.14.0" webview_flutter_android: @@ -959,7 +959,7 @@ packages: description: name: webview_flutter_android sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.12.0" webview_flutter_platform_interface: @@ -967,7 +967,7 @@ packages: description: name: webview_flutter_platform_interface sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.15.1" webview_flutter_wkwebview: @@ -975,7 +975,7 @@ packages: description: name: webview_flutter_wkwebview sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.25.1" win32: @@ -983,7 +983,7 @@ packages: description: name: win32 sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.15.0" xdg_directories: @@ -991,7 +991,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" xml: @@ -999,7 +999,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.6.1" yaml: @@ -1007,7 +1007,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" sdks: diff --git a/pubspec.yaml b/pubspec.yaml index 0be5406..83d41df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,3 +53,4 @@ flutter: assets: - assets/images/ - assets/icon/ + - assets/images/ticket/ diff --git a/参考/票根html/maoyan.png b/参考/票根html/maoyan.png new file mode 100644 index 0000000..a0585d8 Binary files /dev/null and b/参考/票根html/maoyan.png differ diff --git a/参考/票根html/mookiletter.png b/参考/票根html/mookiletter.png new file mode 100644 index 0000000..7ab0463 Binary files /dev/null and b/参考/票根html/mookiletter.png differ diff --git a/参考/票根html/票根.html b/参考/票根html/票根.html new file mode 100644 index 0000000..08bfe46 --- /dev/null +++ b/参考/票根html/票根.html @@ -0,0 +1,395 @@ + + + + + +猫眼电影 · 票根 + + + + +
+
+ + +
+ +
猫眼电影
+
+ + +
+
+
万达影城(朝阳大悦城IMAX店)
+
+ +
+
流浪地球3
+
科幻 / 冒险 / 3D IMAX · 片长148分钟
+
+ +
+
+ + + + + + 3号IMAX厅 +
+
+
+ 8排12座 + | + 2026年6月18日 19:30 +
+
+ + +
+ +
+ + + + +
+
+ + + diff --git a/参考/票根html/票根收藏版.html b/参考/票根html/票根收藏版.html new file mode 100644 index 0000000..a95f3ee --- /dev/null +++ b/参考/票根html/票根收藏版.html @@ -0,0 +1,444 @@ + + + + + +猫眼电影 · 票根(收藏版) + + + + +
+ +
+
+ + 猫眼电影 +
+
收藏纪念
PASSENGER COPY
+
+ +
+
+ + + + + 万达影城(朝阳大悦城IMAX店) +
+ +
流浪地球3
+
+ 3D IMAX + 科幻 / 冒险 + 片长148分钟 +
+ +
+
+ 影厅 + 3号IMAX厅 +
+
+ 座位 + 8排12座 +
+
+ 时间 + 2026/06/18 19:30 +
+
+
+ +
+
+ + + + + + +
+
+ + + + + +
+ + + diff --git a/参考/票根html/票根样式三.html b/参考/票根html/票根样式三.html new file mode 100644 index 0000000..3679ab9 --- /dev/null +++ b/参考/票根html/票根样式三.html @@ -0,0 +1,386 @@ + + + + + +电影票根 · 收藏版 + + + + +
+ + +
+ 猫眼电影 + 收藏纪念
COLLECTIBLE
+
+ + +
+
+ + + + + 万达影城(朝阳大悦城IMAX店) +
+ +
流浪地球3
+
+ 3D IMAX + 科幻 / 冒险 + 片长148分钟 +
+ +
+
+ 影厅 + 3号IMAX厅 +
+
+ 座位 + 8排12座 +
+
+ 时间 + 2026.06.18 19:30 +
+
+
+ + +
+ + +
+ + +
+
+
+ 二维码 +
+
+ +
+
+ 票价 + ¥49.90 +
+
+ 服务费 + ¥5.00 +
+
+
+ 出票时间 + 2026-06-18 10:32 +
+
+ 淘票票 + 已出票 +
+
+ 取票码 + 8602 3100 0618 7251 +
+
+
+ + +
+ TICKET · COLLECTIBLE + NO. MY202606181930 +
+ +
+ + +