Files
app/lib/presentation/pages/preview/document_preview_page.dart

275 lines
9.4 KiB
Dart

import 'dart:ui';
import 'package:cloudreve4_flutter/core/utils/language_preview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:code_text_field/code_text_field.dart';
import 'package:flutter_highlight/themes/atom-one-dark.dart';
import 'package:highlight/highlight.dart';
import 'package:highlight/languages/all.dart';
import 'package:http/http.dart' as http;
import '../../../data/models/file_model.dart';
import '../../../services/custom_line_service.dart';
import '../../../services/file_service.dart';
import '../../../core/utils/file_type_utils.dart';
/// 文档预览页面
class DocumentPreviewPage extends StatefulWidget {
final FileModel file;
final String? entityId;
const DocumentPreviewPage({super.key, required this.file, this.entityId});
@override
State<DocumentPreviewPage> createState() => _DocumentPreviewPageState();
}
class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
CodeController? _codeController;
static const _backgroundColor = Color(0xFF292d3e);
String _content = '';
bool _isLoading = true;
String? _error;
Mode _languageMode = allLanguages['plaintext']!;
String _languageName = 'Text';
int _lineCount = 0;
final ScrollController _customCodeScrollController = ScrollController();
// 状态管理
bool _showLineNumbers = true;
double _fontSize = 14.0; // 默认字体大小
bool _hasInitializedLayout = false;
@override
void initState() {
super.initState();
_loadFileContent();
}
@override
void dispose() {
_codeController?.dispose();
_customCodeScrollController.dispose();
super.dispose();
}
// 加载逻辑保持不变...
Future<void> _loadFileContent() async {
try {
final response = await FileService().getDownloadUrls(
uris: [widget.file.relativePath],
download: true,
entity: widget.entityId,
);
final urls = response['urls'] as List<dynamic>? ?? [];
if (urls.isEmpty) throw Exception('获取URL为空');
final urlData = urls[0] as Map<String, dynamic>;
final url = urlData['url'] as String;
await CustomLineService.instance.activateSelectedNodeForUrl(url);
final responseContent = await http.get(Uri.parse(url));
if (responseContent.statusCode != 200) {
throw Exception('下载文件失败: ${responseContent.statusCode}');
}
if (mounted) {
setState(() {
_content = responseContent.body;
_lineCount = _countLines(_content);
_languageMode = _detectLanguageMode(widget.file.name);
_languageName = _getLanguageNameFromExtension(widget.file.name);
_initCodeController();
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
_isLoading = false;
throw Exception('获取文件内容失败: $_error');
});
}
}
}
void _initCodeController() {
_codeController = CodeController(text: _content, language: _languageMode);
}
int _countLines(String text) =>
text.isEmpty ? 0 : '\n'.allMatches(text).length + 1;
Mode _detectLanguageMode(String fileName) {
final ext = FileTypeUtils.getExtension(fileName);
final extLang = LanguagePreview.getExtNameMapping[ext] ?? ext.toUpperCase();
return allLanguages[extLang.toLowerCase()] ?? allLanguages['plaintext']!;
}
String _getLanguageNameFromExtension(String fileName) {
final ext = FileTypeUtils.getExtension(fileName).toLowerCase();
return LanguagePreview.getExtNameMapping[ext] ?? ext.toUpperCase();
}
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
// 自动初始化布局逻辑
if (!_hasInitializedLayout && !_isLoading) {
_showLineNumbers = screenWidth >= 600;
_hasInitializedLayout = true;
}
// --- 动态行号宽度计算 (核心修复) ---
double lineNumberWidth = 0;
if (_showLineNumbers) {
// 计算行数的位数,并根据当前字体大小分配宽度
int digits = _lineCount.toString().length;
// 这里的 0.7 是字体宽高的约数比例,20 是左右边距预留
lineNumberWidth = (digits * (_fontSize * 0.7)) + 15;
if (lineNumberWidth < 35) lineNumberWidth = 35;
}
return Scaffold(
backgroundColor: _backgroundColor,
appBar: AppBar(
backgroundColor: _backgroundColor,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
title: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.file.name,
style: const TextStyle(color: Colors.white, fontSize: 15),
textAlign: TextAlign.center,
),
if (!_isLoading)
Text(
'$_languageName · $_lineCount 行 · 字号: ${_fontSize.toInt()}',
style: TextStyle(color: Colors.grey.shade400, fontSize: 12),
textAlign: TextAlign.center,
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.copy, color: Colors.white),
onPressed: () => Clipboard.setData(ClipboardData(text: _content)),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white))
: _buildCodeEditor(lineNumberWidth),
floatingActionButton: _buildExpandableFab(),
);
}
/// 现在是一次性渲染的, 所以代码行数太多会有性能问题, 渲染会耗时较长, 而且会卡顿
Widget _buildCodeEditor(double lineNumberWidth) {
// 核心改进:根据当前字号,动态计算一个更宽松的行号宽度
// 13号字大概每个数字占 8-9 像素,我们按 10 像素算并加上边距
int digits = _lineCount.toString().length;
// 这里的 12.0 是根据 13-15号字体的平均宽度预留,46 是基础间距 (调试火葬场)
double stableWidth = _showLineNumbers
? (digits * (_fontSize * 0.75)) + 46
: 0;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: _backgroundColor,
borderRadius: BorderRadius.circular(12),
),
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
child: CodeTheme(
data: CodeThemeData(styles: {...atomOneDarkTheme}),
child: Scrollbar(
controller: _customCodeScrollController,
child: SingleChildScrollView(
controller: _customCodeScrollController,
child: CodeField(
controller: _codeController!,
textStyle: TextStyle(
fontFamily: 'SourceCodePro',
fontSize: _fontSize,
height: 1.5,
),
enabled: false,
readOnly: true,
background: Colors.transparent,
// 关键点 1:调整行号样式
lineNumberStyle: LineNumberStyle(
width: stableWidth,
textAlign: TextAlign.right, // 回归右对齐,更符合代码习惯
margin: _showLineNumbers ? 12 : 0, // 增加间距防止数字贴边触发换行
textStyle: TextStyle(
color: _showLineNumbers
? Colors.grey.shade600
: Colors.transparent,
fontSize: _fontSize * 0.8,
height: 1.5, // 必须和正文高度完全一致
// 核心修复:通过强制单词不换行来防止数字断裂
fontFeatures: const [
FontFeature.tabularFigures(),
], // 使用等宽数字
),
),
),
),
),
),
),
);
}
// 构建功能组合按钮
Widget _buildExpandableFab() {
if (_isLoading) return const SizedBox.shrink();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 增加字号
FloatingActionButton(
heroTag: 'font_up',
mini: true,
backgroundColor: Colors.grey.shade800,
onPressed: () => setState(() {
if (_fontSize < 30) _fontSize++;
}),
child: const Icon(Icons.add, color: Colors.white, size: 20),
),
const SizedBox(height: 8),
// 减小字号
FloatingActionButton(
heroTag: 'font_down',
mini: true,
backgroundColor: Colors.grey.shade800,
onPressed: () => setState(() {
if (_fontSize > 8) _fontSize--;
}),
child: const Icon(Icons.remove, color: Colors.white, size: 20),
),
const SizedBox(height: 8),
// 行号开关
FloatingActionButton(
heroTag: 'line_toggle',
mini: true,
backgroundColor: _showLineNumbers
? Colors.blue
: Colors.grey.shade800,
onPressed: () => setState(() => _showLineNumbers = !_showLineNumbers),
child: Icon(
_showLineNumbers ? Icons.format_list_numbered : Icons.short_text,
color: Colors.white,
size: 20,
),
),
],
);
}
}