自定义线路与线路优化

This commit is contained in:
2026-06-04 17:52:16 +08:00
parent 33509de4e3
commit e6c60a9d6d
16 changed files with 1184 additions and 203 deletions
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../data/models/file_model.dart';
import '../../../services/custom_line_service.dart';
import '../../../services/file_service.dart';
/// 音频预览页面
@@ -43,7 +44,11 @@ class _AudioPreviewPageState extends State<AudioPreviewPage> {
setState(() {
_isLoading = false;
});
player.open(Media(url), play: true);
await CustomLineService.instance.configureMediaPlayerProxy(
player,
url,
);
await player.open(Media(url), play: true);
}
} else {
if (mounted) {
@@ -104,11 +109,7 @@ class _AudioPreviewPageState extends State<AudioPreviewPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.white54,
),
const Icon(Icons.error_outline, size: 64, color: Colors.white54),
const SizedBox(height: 16),
Text(
_errorMessage!,
@@ -140,14 +141,17 @@ class AudioPlayerWidget extends StatefulWidget {
final Player player;
final String fileName;
const AudioPlayerWidget({super.key, required this.player, required this.fileName});
const AudioPlayerWidget({
super.key,
required this.player,
required this.fileName,
});
@override
State<AudioPlayerWidget> createState() => _AudioPlayerWidgetState();
}
class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
@override
Widget build(BuildContext context) {
return Container(
@@ -155,10 +159,7 @@ class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFF1A1A2E),
const Color(0xFF16213E),
],
colors: [const Color(0xFF1A1A2E), const Color(0xFF16213E)],
),
),
child: Padding(
@@ -246,12 +247,18 @@ class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
return SliderTheme(
data: SliderThemeData(
trackHeight: 4,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 16),
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8,
),
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 16,
),
activeTrackColor: const Color(0xFFE94560),
inactiveTrackColor: Colors.white.withValues(alpha: 0.1),
thumbColor: const Color(0xFFE94560),
overlayColor: const Color(0xFFE94560).withValues(alpha: 0.3),
overlayColor: const Color(
0xFFE94560,
).withValues(alpha: 0.3),
),
child: Slider(
value: position.inMilliseconds.toDouble(),
@@ -259,9 +266,7 @@ class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
? duration.inMilliseconds.toDouble()
: 1.0,
onChanged: (value) {
widget.player.seek(
Duration(milliseconds: value.toInt()),
);
widget.player.seek(Duration(milliseconds: value.toInt()));
},
),
);
@@ -8,6 +8,7 @@ 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';
@@ -62,6 +63,7 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
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) {
@@ -93,7 +95,8 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
_codeController = CodeController(text: _content, language: _languageMode);
}
int _countLines(String text) => text.isEmpty ? 0 : '\n'.allMatches(text).length + 1;
int _countLines(String text) =>
text.isEmpty ? 0 : '\n'.allMatches(text).length + 1;
Mode _detectLanguageMode(String fileName) {
final ext = FileTypeUtils.getExtension(fileName);
@@ -109,7 +112,7 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
// 自动初始化布局逻辑
if (!_hasInitializedLayout && !_isLoading) {
_showLineNumbers = screenWidth >= 600;
@@ -122,7 +125,7 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
// 计算行数的位数,并根据当前字体大小分配宽度
int digits = _lineCount.toString().length;
// 这里的 0.7 是字体宽高的约数比例,20 是左右边距预留
lineNumberWidth = (digits * (_fontSize * 0.7)) + 15;
lineNumberWidth = (digits * (_fontSize * 0.7)) + 15;
if (lineNumberWidth < 35) lineNumberWidth = 35;
}
@@ -131,16 +134,21 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
appBar: AppBar(
backgroundColor: _backgroundColor,
elevation: 0,
iconTheme: const IconThemeData(
color: Colors.white,
),
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,),
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: [
@@ -159,60 +167,64 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
/// 现在是一次性渲染的, 所以代码行数太多会有性能问题, 渲染会耗时较长, 而且会卡顿
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),
// 核心改进:根据当前字号,动态计算一个更宽松的行号宽度
// 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: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
child: CodeTheme(
data: CodeThemeData(styles: {...atomOneDarkTheme}),
child: Scrollbar(
child: CodeTheme(
data: CodeThemeData(styles: {...atomOneDarkTheme}),
child: Scrollbar(
controller: _customCodeScrollController,
child: SingleChildScrollView(
controller: _customCodeScrollController,
child: SingleChildScrollView(
controller: _customCodeScrollController,
child: CodeField(
controller: _codeController!,
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(
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()], // 使用等宽数字
),
color: _showLineNumbers
? Colors.grey.shade600
: Colors.transparent,
fontSize: _fontSize * 0.8,
height: 1.5, // 必须和正文高度完全一致
// 核心修复:通过强制单词不换行来防止数字断裂
fontFeatures: const [
FontFeature.tabularFigures(),
], // 使用等宽数字
),
),
),
),
),
),
);
}
),
);
}
// 构建功能组合按钮
Widget _buildExpandableFab() {
@@ -225,7 +237,9 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
heroTag: 'font_up',
mini: true,
backgroundColor: Colors.grey.shade800,
onPressed: () => setState(() { if(_fontSize < 30) _fontSize++; }),
onPressed: () => setState(() {
if (_fontSize < 30) _fontSize++;
}),
child: const Icon(Icons.add, color: Colors.white, size: 20),
),
const SizedBox(height: 8),
@@ -234,7 +248,9 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
heroTag: 'font_down',
mini: true,
backgroundColor: Colors.grey.shade800,
onPressed: () => setState(() { if(_fontSize > 8) _fontSize--; }),
onPressed: () => setState(() {
if (_fontSize > 8) _fontSize--;
}),
child: const Icon(Icons.remove, color: Colors.white, size: 20),
),
const SizedBox(height: 8),
@@ -242,7 +258,9 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
FloatingActionButton(
heroTag: 'line_toggle',
mini: true,
backgroundColor: _showLineNumbers ? Colors.blue : Colors.grey.shade800,
backgroundColor: _showLineNumbers
? Colors.blue
: Colors.grey.shade800,
onPressed: () => setState(() => _showLineNumbers = !_showLineNumbers),
child: Icon(
_showLineNumbers ? Icons.format_list_numbered : Icons.short_text,
@@ -253,4 +271,4 @@ class _DocumentPreviewPageState extends State<DocumentPreviewPage> {
],
);
}
}
}
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:photo_view/photo_view.dart';
import '../../../data/models/file_model.dart';
import '../../../services/custom_line_service.dart';
import '../../../services/file_service.dart';
import '../../../services/cache_manager_service.dart';
import '../../widgets/toast_helper.dart';
@@ -44,6 +45,7 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
if (urls.isNotEmpty) {
final urlData = urls[0] as Map<String, dynamic>;
final url = urlData['url'] as String;
await CustomLineService.instance.activateSelectedNodeForUrl(url);
if (mounted) {
setState(() {
@@ -197,7 +199,7 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
// 2. 编写平滑缩放函数
void _smoothScale(PhotoViewController photoController, double delta) {
// 如果正在动画中,忽略新的滚轮脉冲,防止冲突
if (_isAnimating) return;
if (_isAnimating) return;
_isAnimating = true;
// 计算目标缩放值
@@ -11,6 +11,7 @@ import 'package:markdown_widget/widget/blocks/leaf/link.dart';
import 'package:markdown_widget/widget/markdown.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../data/models/file_model.dart';
import '../../../services/custom_line_service.dart';
import '../../../services/file_service.dart';
class MarkdownPreviewPage extends StatefulWidget {
@@ -30,7 +31,7 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
final ScrollController _scrollController = ScrollController();
final _tocController = TocController();
bool _isDarkMode = false;
// 控制目录是否显示
bool _isTocVisible = false;
// 标记是否已经根据屏幕宽度进行了初始化
@@ -62,6 +63,7 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
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) {
@@ -104,7 +106,10 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
backgroundColor: bgColor,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: isDark ? Colors.white : Colors.black87),
icon: Icon(
Icons.arrow_back,
color: isDark ? Colors.white : Colors.black87,
),
onPressed: () => Navigator.of(context).pop(),
),
title: Column(
@@ -120,13 +125,19 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
overflow: TextOverflow.ellipsis,
),
if (!_isLoading && _error == null)
Text('Markdown 预览', style: TextStyle(color: Colors.grey, fontSize: 12)),
Text(
'Markdown 预览',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
actions: [
if (!_isLoading && _error == null)
IconButton(
icon: Icon(Icons.copy, color: isDark ? Colors.white : Colors.black87),
icon: Icon(
Icons.copy,
color: isDark ? Colors.white : Colors.black87,
),
onPressed: () => Clipboard.setData(ClipboardData(text: _content)),
),
],
@@ -134,8 +145,8 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? _buildErrorWidget()
: _buildResponsiveBody(isWideScreen, screenWidth),
? _buildErrorWidget()
: _buildResponsiveBody(isWideScreen, screenWidth),
floatingActionButton: _buildFAB(isDark),
);
}
@@ -143,7 +154,7 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
// 构建响应式主体
Widget _buildResponsiveBody(bool isWideScreen, double screenWidth) {
final double tocWidth = isWideScreen ? 300 : screenWidth * 0.7;
// 预定义暗色/亮色下的文字颜色
final Color textColor = _isDarkMode ? Colors.white : Colors.black87;
final Color subTextColor = _isDarkMode ? Colors.white70 : Colors.black54;
@@ -159,10 +170,14 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: _isDarkMode ? Colors.white10 : Colors.grey.withValues(alpha: 0.2)
color: _isDarkMode
? Colors.white10
: Colors.grey.withValues(alpha: 0.2),
),
),
color: _isDarkMode ? const Color(0xFF252525) : Colors.grey.shade50,
color: _isDarkMode
? const Color(0xFF252525)
: Colors.grey.shade50,
),
child: ClipRect(
child: Column(
@@ -178,7 +193,10 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
),
),
),
Divider(height: 1, color: _isDarkMode ? Colors.white10 : Colors.grey.shade300),
Divider(
height: 1,
color: _isDarkMode ? Colors.white10 : Colors.grey.shade300,
),
Expanded(
child: GestureDetector(
onTap: () {
@@ -198,9 +216,7 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
bodySmall: TextStyle(color: subTextColor),
),
),
child: TocWidget(
controller: _tocController,
),
child: TocWidget(controller: _tocController),
),
),
),
@@ -228,8 +244,11 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
}
Widget _buildMarkdownWidget(String content) {
final config = _isDarkMode ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig;
CodeWrapperWidget codeWrapper(child, text, language) => CodeWrapperWidget(child, text, language);
final config = _isDarkMode
? MarkdownConfig.darkConfig
: MarkdownConfig.defaultConfig;
CodeWrapperWidget codeWrapper(child, text, language) =>
CodeWrapperWidget(child, text, language);
return MarkdownWidget(
data: content,
@@ -237,7 +256,10 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
config: config.copy(
configs: [
_isDarkMode
? PreConfig.darkConfig.copy(theme: a11yLightTheme, wrapper: codeWrapper)
? PreConfig.darkConfig.copy(
theme: a11yLightTheme,
wrapper: codeWrapper,
)
: PreConfig(theme: atomOneDarkTheme).copy(wrapper: codeWrapper),
LinkConfig(
style: TextStyle(
@@ -266,12 +288,14 @@ class _MarkdownPreviewPageState extends State<MarkdownPreviewPage> {
);
}
Widget _buildFAB(bool isDark) {
Widget _buildFAB(bool isDark) {
if (_isLoading || _error != null) return const SizedBox.shrink();
// 定义统一的按钮背景颜色,增加视觉一致性
final Color activeColor = isDark ? Colors.blue.shade700 : Colors.blue;
final Color inactiveColor = isDark ? Colors.grey.shade800 : Colors.grey.shade200;
final Color inactiveColor = isDark
? Colors.grey.shade800
: Colors.grey.shade200;
final Color iconColor = isDark ? Colors.white : Colors.black87;
return Column(
@@ -285,7 +309,7 @@ Widget _buildFAB(bool isDark) {
elevation: 2, // 稍微降低阴影,看起来更精致
onPressed: () => setState(() => _isDarkMode = !_isDarkMode),
child: Icon(
isDark ? Icons.light_mode : Icons.dark_mode,
isDark ? Icons.light_mode : Icons.dark_mode,
color: iconColor,
size: 20, // 微调图标大小
),
@@ -300,7 +324,7 @@ Widget _buildFAB(bool isDark) {
elevation: 2,
onPressed: () => setState(() => _isTocVisible = !_isTocVisible),
child: Icon(
Icons.format_list_bulleted,
Icons.format_list_bulleted,
color: _isTocVisible ? Colors.white : iconColor,
size: 20,
),
@@ -308,4 +332,4 @@ Widget _buildFAB(bool isDark) {
],
);
}
}
}
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:pdfrx/pdfrx.dart';
import '../../../data/models/file_model.dart';
import '../../../services/custom_line_service.dart';
import '../../../services/file_service.dart';
/// PDF预览页面
@@ -37,6 +38,7 @@ class _PdfPreviewPageState extends State<PdfPreviewPage> {
if (urls.isNotEmpty) {
final urlData = urls[0] as Map<String, dynamic>;
final url = urlData['url'] as String;
await CustomLineService.instance.activateSelectedNodeForUrl(url);
if (mounted) {
setState(() {
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import '../../../data/models/file_model.dart';
import '../../../services/custom_line_service.dart';
import '../../../services/file_service.dart';
import 'widgets/video_controls_overlay.dart';
@@ -45,7 +46,11 @@ class _VideoPreviewPageState extends State<VideoPreviewPage> {
if (mounted) {
setState(() => _isLoading = false);
player.open(Media(url), play: true);
await CustomLineService.instance.configureMediaPlayerProxy(
player,
url,
);
await player.open(Media(url), play: true);
}
} else {
if (mounted) {
@@ -78,37 +83,42 @@ class _VideoPreviewPageState extends State<VideoPreviewPage> {
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.white))
: _errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.white),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
setState(() {
_isLoading = true;
_errorMessage = null;
});
_loadVideoUrl();
},
child: const Text('重试'),
),
],
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.white,
),
)
: ExcludeSemantics(
child: Video(
controller: controller,
controls: (state) => VideoControlsOverlay(state: state, title: widget.file.name),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white70),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
setState(() {
_isLoading = true;
_errorMessage = null;
});
_loadVideoUrl();
},
child: const Text('重试'),
),
],
),
)
: ExcludeSemantics(
child: Video(
controller: controller,
controls: (state) =>
VideoControlsOverlay(state: state, title: widget.file.name),
),
),
);
}
}