Files
MookNote/lib/pages/game/game_screenshots_page.dart
2026-08-12 22:28:33 +08:00

369 lines
14 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:http/http.dart' as http;
import '../../providers/app_provider.dart';
import '../../widgets/fade_in_local_image.dart';
import 'package:uuid/uuid.dart';
import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart';
import 'screenshot_gallery_page.dart';
import '../../widgets/app_overlay.dart';
/// 游戏截图页面
class GameScreenshotsPage extends StatefulWidget {
final Game game;
const GameScreenshotsPage({super.key, required this.game});
@override
State<GameScreenshotsPage> createState() => _GameScreenshotsPageState();
}
class _GameScreenshotsPageState extends State<GameScreenshotsPage> {
final ImagePicker _picker = ImagePicker();
List<GameScreenshot> _screenshots = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadScreenshots();
}
Future<void> _loadScreenshots() async {
setState(() => _isLoading = true);
final screenshots = await context.read<AppProvider>().getGameScreenshots(widget.game.id);
setState(() {
_screenshots = screenshots;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(title: const Text('游戏截图')),
floatingActionButton: FloatingActionButton.extended(
onPressed: _pickScreenshot,
icon: const Icon(Icons.add_photo_alternate, size: 20),
label: const Text('添加截图'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _screenshots.isEmpty
? _buildEmptyState()
: _buildScreenshotGrid(),
);
}
Widget _buildEmptyState() {
final colors = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20)),
child: Icon(Icons.photo_library_outlined, size: 40, color: colors.onSurface.withValues(alpha: 0.25)),
),
const SizedBox(height: 20),
Text('暂无截图', style: TextStyle(fontSize: 16, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 24),
],
),
);
}
Widget _buildScreenshotGrid() {
return MasonryGridView.count(
padding: const EdgeInsets.all(16),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: _screenshots.length,
itemBuilder: (context, index) {
final screenshot = _screenshots[index];
return _buildScreenshotItem(screenshot, index);
},
);
}
Widget _buildScreenshotItem(GameScreenshot screenshot, int index) {
final heights = [180.0, 220.0, 160.0, 200.0, 240.0, 190.0];
final height = heights[index % heights.length];
return GestureDetector(
onTap: () => _showScreenshotDetail(screenshot),
onLongPress: () => _showDeleteDialog(screenshot),
child: Container(
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2))],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
FadeInLocalImage(path: screenshot.screenshotPath, fit: BoxFit.cover),
Positioned(
bottom: 0, left: 0, right: 0,
child: Container(
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter, end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black.withValues(alpha: 0.3)],
),
),
),
),
],
),
),
),
);
}
void _showScreenshotDetail(GameScreenshot screenshot) {
final initialIndex = _screenshots.indexWhere((s) => s.id == screenshot.id);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenshotGalleryPage(
screenshots: _screenshots,
initialIndex: initialIndex >= 0 ? initialIndex : 0,
),
),
);
}
Future<void> _pickScreenshot() async {
final result = await appModalBottomSheet<int>(
context: context,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (context) {
final colors = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(color: colors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(16))),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 40, height: 4, decoration: BoxDecoration(color: colors.outline, borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(children: [
Text('添加截图', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
),
const SizedBox(height: 16),
_buildAddOption(colors: colors, icon: Icons.photo_library_outlined, title: '从相册选择', subtitle: '选择本地图片', onTap: () => Navigator.pop(context, 0)),
_buildAddOption(colors: colors, icon: Icons.link_outlined, title: '网络链接', subtitle: '输入图片URL地址', onTap: () => Navigator.pop(context, 1)),
],
),
),
),
);
},
);
if (result == null) return;
if (result == 0) {
await _pickFromGallery();
} else if (result == 1) {
await _pickFromUrl();
}
}
Widget _buildAddOption({required ColorScheme colors, required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
child: Row(
children: [
Container(
width: 44, height: 44,
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
child: Icon(icon, size: 22, color: colors.onSurface.withValues(alpha: 0.6)),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(subtitle, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
],
),
),
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
],
),
),
);
}
Future<void> _pickFromGallery() async {
try {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1200, maxHeight: 1800, imageQuality: 85,
);
if (pickedFile != null) {
final fileName = 'screenshot_${DateTime.now().millisecondsSinceEpoch}.jpg';
final targetPath = await ImagePathHelper.instance.getGameScreenshotImgPath(widget.game.id, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(pickedFile.path).copy(targetPath);
final newScreenshot = GameScreenshot(
id: const Uuid().v4(),
gameId: widget.game.id,
screenshotPath: targetPath,
createdAt: DateTime.now(),
);
await context.read<AppProvider>().addGameScreenshot(newScreenshot);
_loadScreenshots();
if (mounted) ToastUtil.show(context, '添加成功');
}
} catch (e) {
if (mounted) ToastUtil.show(context, '添加截图失败: $e');
}
}
Future<void> _pickFromUrl() async {
final urlController = TextEditingController();
final confirmed = await appDialog<bool>(
context: context,
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface, elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('添加网络图片'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('请输入图片链接地址', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
const SizedBox(height: 12),
TextField(
controller: urlController,
decoration: InputDecoration(
hintText: 'https://example.com/image.jpg',
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.25)),
border: const UnderlineInputBorder(),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: colors.outline)),
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: colors.primary)),
),
style: const TextStyle(fontSize: 14),
keyboardType: TextInputType.url,
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
TextButton(onPressed: () => Navigator.pop(context, true), child: Text('确定', style: TextStyle(color: colors.onSurface))),
],
);
},
);
if (confirmed != true) return;
final url = urlController.text.trim();
WidgetsBinding.instance.addPostFrameCallback((_) {
urlController.dispose();
});
if (url.isEmpty) { if (mounted) ToastUtil.show(context, '请输入图片链接'); return; }
try {
await _downloadAndSaveScreenshot(url);
} catch (e) {
if (mounted) ToastUtil.show(context, '添加失败: $e');
}
}
Future<void> _downloadAndSaveScreenshot(String url) async {
try {
final response = await http.get(
Uri.parse(url),
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Referer': Uri.parse(url).replace(path: '/').toString(),
},
);
if (response.statusCode != 200) throw Exception('下载失败: HTTP ${response.statusCode}');
final contentType = response.headers['content-type'];
if (contentType != null && !contentType.startsWith('image/')) throw Exception('链接返回的不是图片');
if (response.bodyBytes.length > 10 * 1024 * 1024) throw Exception('图片太大');
final fileName = 'screenshot_${DateTime.now().millisecondsSinceEpoch}.jpg';
final targetPath = await ImagePathHelper.instance.getGameScreenshotImgPath(widget.game.id, fileName);
await ImagePathHelper.instance.ensureDirExists(p.dirname(targetPath));
await File(targetPath).writeAsBytes(response.bodyBytes);
final newScreenshot = GameScreenshot(
id: const Uuid().v4(),
gameId: widget.game.id,
screenshotPath: targetPath,
createdAt: DateTime.now(),
);
await context.read<AppProvider>().addGameScreenshot(newScreenshot);
_loadScreenshots();
if (mounted) ToastUtil.show(context, '添加成功');
} catch (e) {
throw Exception('下载图片失败: $e');
}
}
void _showDeleteDialog(GameScreenshot screenshot) {
appDialog(
context: context,
builder: (context) {
final colors = Theme.of(context).colorScheme;
return AlertDialog(
backgroundColor: colors.surface, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
content: Text('确定要删除这张截图吗?',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
ElevatedButton(
onPressed: () async {
await context.read<AppProvider>().removeGameScreenshot(screenshot.id);
Navigator.pop(context);
_loadScreenshots();
ToastUtil.show(context, '已删除');
},
style: ElevatedButton.styleFrom(
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('删除'),
),
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
);
},
);
}
}