275 lines
9.4 KiB
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,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|