编辑界面优化

This commit is contained in:
DelLevin-Home
2026-03-22 15:54:28 +08:00
parent f9ea3d8284
commit 0c18df3943
9 changed files with 2284 additions and 595 deletions

View File

@@ -318,6 +318,8 @@ class _DoubanWebViewPageState extends State<DoubanWebViewPage> {
if (roleEl && (
roleEl.textContent.includes('配音') ||
roleEl.textContent.includes('主演') ||
roleEl.textContent.includes('演员') ||
roleEl.textContent.includes('参演') ||
roleEl.textContent.includes('饰')
)) {
const nameEl = el.querySelector('.name');

View File

@@ -11,6 +11,7 @@ import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import 'movie_reviews_page.dart';
import 'movie_posters_page.dart';
import 'movie_share_page.dart';
/// 影视详情页 - 极简主义设计
class MovieDetailPage extends StatefulWidget {
@@ -45,87 +46,125 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
return Scaffold(
backgroundColor: Colors.white,
body: CustomScrollView(
slivers: [
// 顶部海报区域
_buildSliverAppBar(movie),
body: Stack(
children: [
CustomScrollView(
slivers: [
// 顶部海报区域
_buildSliverAppBar(movie),
// 内容区域
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本信息
_buildBasicInfo(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 导演
if (movie.directors.isNotEmpty)
_buildDirectorsSection(movie),
// 编剧
if (movie.writers.isNotEmpty)
_buildWritersSection(movie),
// 主演
if (movie.actors.isNotEmpty)
_buildActorsSection(movie),
// 类型
if (movie.genres.isNotEmpty)
_buildGenresSection(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
if (movie.summary != null && movie.summary!.isNotEmpty)
_buildSummarySection(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 影评和海报墙入口
_buildExtraSections(movie),
const SizedBox(height: 120),
],
),
),
],
),
// 内容区域
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本信息
_buildBasicInfo(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 导演
if (movie.directors.isNotEmpty)
_buildDirectorsSection(movie),
// 编剧
if (movie.writers.isNotEmpty)
_buildWritersSection(movie),
// 主演
if (movie.actors.isNotEmpty)
_buildActorsSection(movie),
// 类型
if (movie.genres.isNotEmpty)
_buildGenresSection(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
if (movie.summary != null && movie.summary!.isNotEmpty)
_buildSummarySection(movie),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 影评和海报墙入口
_buildExtraSections(movie),
const SizedBox(height: 48),
],
),
// 右下角悬浮按钮组
Positioned(
right: 16,
bottom: 24,
child: _buildFloatingActionButtons(movie),
),
],
),
// 底部无操作栏,编辑和删除在右上角
);
}
/// 构建右上角操作按钮
Widget _buildActionButton({
/// 构建右下角悬浮按钮
Widget _buildFloatingActionButtons(Movie movie) {
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 删除按钮
_buildFloatingButton(
icon: Icons.delete_outline,
onPressed: () => _showDeleteDialog(context),
tooltip: '删除',
backgroundColor: Colors.red,
),
const SizedBox(height: 12),
// 清空海报按钮(仅当有海报时显示)
if (hasPoster) ...[
_buildFloatingButton(
icon: Icons.hide_image_outlined,
onPressed: () => _showClearPosterDialog(movie),
tooltip: '清空海报',
),
const SizedBox(height: 12),
],
// 编辑按钮
_buildFloatingButton(
icon: Icons.edit_outlined,
onPressed: () => _navigateToEdit(context),
tooltip: '编辑',
),
const SizedBox(height: 12),
// 分享按钮
_buildFloatingButton(
icon: Icons.share_outlined,
onPressed: () => _showSharePoster(movie),
tooltip: '分享海报',
backgroundColor: const Color(0xFF4CAF50),
),
],
);
}
/// 构建单个悬浮按钮
Widget _buildFloatingButton({
required IconData icon,
required VoidCallback onPressed,
required String tooltip,
Color color = const Color(0xFF666666),
Color backgroundColor = const Color(0xFF1A1A1A),
}) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(8),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
icon,
color: color,
size: 22,
),
),
),
),
return FloatingActionButton(
onPressed: onPressed,
tooltip: tooltip,
backgroundColor: backgroundColor,
foregroundColor: Colors.white,
mini: true,
elevation: 4,
child: Icon(icon, size: 20),
);
}
@@ -157,8 +196,6 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
/// 构建顶部 AppBar
Widget _buildSliverAppBar(Movie movie) {
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return SliverAppBar(
expandedHeight: 320,
pinned: true,
@@ -167,29 +204,7 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
flexibleSpace: FlexibleSpaceBar(
background: _buildPosterSection(movie),
),
actions: [
// 清空海报按钮(仅当有海报时显示)
if (hasPoster)
_buildActionButton(
icon: Icons.hide_image_outlined,
onPressed: () => _showClearPosterDialog(movie),
tooltip: '清空海报',
),
// 编辑按钮
_buildActionButton(
icon: Icons.edit_outlined,
onPressed: () => _navigateToEdit(context),
tooltip: '编辑',
),
// 删除按钮
_buildActionButton(
icon: Icons.delete_outline,
color: Colors.red,
onPressed: () => _showDeleteDialog(context),
tooltip: '删除',
),
const SizedBox(width: 8),
],
// 右上角按钮已移到右下角悬浮按钮
);
}
@@ -850,4 +865,14 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
// 这里默认返回较低版本,使用传统存储权限
return 30;
}
/// 显示分享海报页面
void _showSharePoster(Movie movie) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MovieSharePage(movie: movie),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,320 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
/// 影视分享海报页面
class MovieSharePage extends StatefulWidget {
final Movie movie;
const MovieSharePage({super.key, required this.movie});
@override
State<MovieSharePage> createState() => _MovieSharePageState();
}
class _MovieSharePageState extends State<MovieSharePage> {
final GlobalKey _posterKey = GlobalKey();
bool _isGenerating = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Color(0xFF1A1A1A)),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'分享海报',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
centerTitle: true,
actions: [
TextButton(
onPressed: _isGenerating ? null : _generateAndShare,
child: _isGenerating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text(
'分享',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
),
const SizedBox(width: 8),
],
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: RepaintBoundary(
key: _posterKey,
child: _buildPosterWidget(),
),
),
),
);
}
/// 构建海报 Widget
Widget _buildPosterWidget() {
final movie = widget.movie;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
return Container(
width: 320,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(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: Image.file(
File(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: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF1A1A1A),
),
),
// 别名
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
movie.alternateTitles.join(' / '),
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
),
],
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),
const Text(
'/ 10',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
const SizedBox(height: 12),
],
// 导演
if (movie.directors.isNotEmpty)
_buildInfoRow('导演', movie.directors.join(' / ')),
// 编剧
if (movie.writers.isNotEmpty)
_buildInfoRow('编剧', movie.writers.join(' / ')),
// 主演
if (movie.actors.isNotEmpty)
_buildInfoRow('主演', movie.actors.take(3).join(' / ')),
// 类型
if (movie.genres.isNotEmpty)
_buildInfoRow('类型', movie.genres.join(' / ')),
// 上映日期
if (movie.releaseDate != null)
_buildInfoRow(
'上映',
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
),
const SizedBox(height: 16),
// 简介
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
const Text(
'简介',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
const SizedBox(height: 8),
Text(
movie.summary!,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
height: 1.6,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 20),
// 底部标识
const Divider(height: 1, color: Color(0xFFE8E8E8)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.movie_outlined,
size: 14,
color: const Color(0xFF1A1A1A).withOpacity(0.5),
),
const SizedBox(width: 6),
Text(
'来自 MookNote',
style: TextStyle(
fontSize: 12,
color: const Color(0xFF1A1A1A).withOpacity(0.5),
),
),
],
),
],
),
),
],
),
);
}
/// 构建信息行
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label',
style: const TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF333333),
),
),
),
],
),
);
}
/// 生成并分享海报
Future<void> _generateAndShare() async {
setState(() => _isGenerating = true);
try {
// 捕获海报图片
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();
// 保存到临时文件
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}',
);
} catch (e) {
if (mounted) {
ToastUtil.show(context, '生成海报失败:$e');
}
} finally {
if (mounted) {
setState(() => _isGenerating = false);
}
}
}
}