二次开发源码提交
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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/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;
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user