upd
1
.gitignore
vendored
@@ -6,6 +6,7 @@ logs/
|
|||||||
config/
|
config/
|
||||||
test/
|
test/
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
image_cache/
|
image_cache/
|
||||||
download/
|
download/
|
||||||
text_cache/
|
text_cache/
|
||||||
|
|||||||
39
LeonPan.spec
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('D:\\Projects\\Python\\LeonPan PC\\_internal', '_internal'), ('D:\\Projects\\Python\\LeonPan PC\\logo.png', 'logo.png'), ('D:\\Projects\\Python\\LeonPan PC\\welcome_video.py', 'welcome_video.py'), ('D:\\Projects\\Python\\LeonPan PC\\app\\resource', 'app/resource')],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='LeonPan',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['D:\\Projects\\Python\\LeonPan PC\\logo.png'],
|
||||||
|
)
|
||||||
39
LeonPan_Console.spec
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('D:\\Projects\\Python\\LeonPan PC\\_internal', '_internal'), ('D:\\Projects\\Python\\LeonPan PC\\logo.png', 'logo.png'), ('D:\\Projects\\Python\\LeonPan PC\\welcome_video.py', 'welcome_video.py'), ('D:\\Projects\\Python\\LeonPan PC\\app\\resource', 'app/resource')],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='LeonPan_Console',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['D:\\Projects\\Python\\LeonPan PC\\logo.png'],
|
||||||
|
)
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
1. 09-88-4069
|
|
||||||
2. 72-33-8317
|
|
||||||
3. 93-54-4878
|
|
||||||
4. 93-86-1869
|
|
||||||
5. 14-04-1203
|
|
||||||
6. 84-00-2929
|
|
||||||
7. 04-82-6399
|
|
||||||
8. 79-11-1229
|
|
||||||
9. 11-06-1474
|
|
||||||
10. 07-72-4607
|
|
||||||
11. 56-87-7920
|
|
||||||
12. 53-25-2426
|
|
||||||
13. 67-39-6157
|
|
||||||
14. 63-39-2233
|
|
||||||
15. 17-96-4158
|
|
||||||
16. 17-62-6178
|
|
||||||
17. 15-37-0827
|
|
||||||
18. 59-52-8112
|
|
||||||
19. 03-21-4560
|
|
||||||
20. 55-11-5109
|
|
||||||
21. 67-99-2556
|
|
||||||
22. 91-20-2424
|
|
||||||
23. 22-43-2052
|
|
||||||
24. 77-64-5252
|
|
||||||
25. 59-35-8073
|
|
||||||
26. 55-31-5493
|
|
||||||
27. 32-76-3175
|
|
||||||
28. 84-91-2685
|
|
||||||
29. 87-12-2589
|
|
||||||
30. 80-19-6183
|
|
||||||
31. 01-53-7563
|
|
||||||
32. 48-27-6989
|
|
||||||
33. 11-13-8688
|
|
||||||
34. 09-54-6589
|
|
||||||
35. 07-23-1493
|
|
||||||
36. 59-42-4357
|
|
||||||
37. 35-19-0434
|
|
||||||
38. 14-36-4400
|
|
||||||
39. 02-87-5750
|
|
||||||
40. 70-48-9729
|
|
||||||
41. 72-69-7715
|
|
||||||
42. 46-99-7577
|
|
||||||
43. 38-14-9272
|
|
||||||
44. 40-42-7732
|
|
||||||
45. 96-91-2081
|
|
||||||
46. 23-68-6080
|
|
||||||
47. 91-39-5462
|
|
||||||
48. 19-92-2732
|
|
||||||
49. 72-23-5102
|
|
||||||
50. 27-88-0690
|
|
||||||
51. 15-45-5394
|
|
||||||
52. 05-99-9553
|
|
||||||
53. 99-05-6922
|
|
||||||
54. 01-89-7062
|
|
||||||
55. 48-19-1418
|
|
||||||
56. 55-07-8466
|
|
||||||
57. 96-22-4957
|
|
||||||
58. 53-82-8031
|
|
||||||
59. 18-11-2941
|
|
||||||
60. 98-93-7633
|
|
||||||
61. 63-94-6925
|
|
||||||
62. 12-74-5129
|
|
||||||
63. 34-17-2602
|
|
||||||
64. 03-04-7664
|
|
||||||
65. 90-38-9969
|
|
||||||
66. 24-89-8276
|
|
||||||
67. 97-89-9536
|
|
||||||
68. 80-32-7705
|
|
||||||
69. 72-32-7795
|
|
||||||
70. 12-80-4790
|
|
||||||
71. 73-84-9545
|
|
||||||
72. 91-03-5053
|
|
||||||
73. 52-40-1281
|
|
||||||
74. 74-75-2213
|
|
||||||
75. 17-50-3710
|
|
||||||
76. 60-36-2783
|
|
||||||
77. 98-57-1091
|
|
||||||
78. 83-59-9231
|
|
||||||
79. 29-48-8192
|
|
||||||
80. 12-91-0807
|
|
||||||
81. 74-25-2849
|
|
||||||
82. 64-82-0610
|
|
||||||
83. 10-23-2036
|
|
||||||
84. 91-63-7704
|
|
||||||
85. 28-68-1631
|
|
||||||
86. 47-23-2650
|
|
||||||
87. 38-04-5634
|
|
||||||
88. 96-08-5695
|
|
||||||
89. 03-56-8427
|
|
||||||
90. 39-51-0578
|
|
||||||
91. 44-87-6122
|
|
||||||
92. 64-20-1925
|
|
||||||
93. 93-14-6077
|
|
||||||
94. 98-76-4163
|
|
||||||
95. 94-60-5635
|
|
||||||
96. 52-43-0973
|
|
||||||
97. 33-16-2106
|
|
||||||
98. 12-66-8985
|
|
||||||
99. 24-57-6007
|
|
||||||
100. 40-23-4384
|
|
||||||
<##-##-####>
|
|
||||||
|
Before Width: | Height: | Size: 9.3 MiB After Width: | Height: | Size: 9.3 MiB |
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 11 MiB |
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 7.9 MiB |
|
Before Width: | Height: | Size: 5.9 MiB After Width: | Height: | Size: 5.9 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
@@ -68,7 +68,7 @@
|
|||||||
let tempFilePath = '';
|
let tempFilePath = '';
|
||||||
|
|
||||||
// 初始化Monaco Editor
|
// 初始化Monaco Editor
|
||||||
require(['vs/editor/editor.main'], function () {
|
require(['vs/editor/editor.main'], function() {
|
||||||
// 从URL参数获取内容和语言
|
// 从URL参数获取内容和语言
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const contentParam = urlParams.get('content');
|
const contentParam = urlParams.get('content');
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
initialContent = Base.decode(contentParam);
|
initialContent = Base.decode(contentParam);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解码内容失败:', e);
|
console.error('解码内容失败:', e);
|
||||||
|
initialContent = contentParam; // 如果解码失败,使用原始内容
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,28 +90,33 @@
|
|||||||
editor = monaco.editor.create(document.getElementById('container'), {
|
editor = monaco.editor.create(document.getElementById('container'), {
|
||||||
value: initialContent,
|
value: initialContent,
|
||||||
language: languageParam,
|
language: languageParam,
|
||||||
theme: 'vs-dark',
|
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
minimap: { enabled: true },
|
minimap: { enabled: true },
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
wordWrap: 'on',
|
fontFamily: 'Consolas, "Courier New", monospace',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
readOnly: false,
|
roundedSelection: true,
|
||||||
fontFamily: 'Consolas, "Microsoft YaHei", monospace'
|
scrollbar: {
|
||||||
|
useShadows: false,
|
||||||
|
verticalScrollbarSize: 10,
|
||||||
|
horizontalScrollbarSize: 10
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加内容变化监听器
|
// 监听窗口大小变化
|
||||||
editor.onDidChangeModelContent(function() {
|
window.addEventListener('resize', function() {
|
||||||
// 内容变化处理
|
|
||||||
});
|
|
||||||
|
|
||||||
// 窗口大小改变时重新布局
|
|
||||||
window.onresize = function () {
|
|
||||||
if (editor) {
|
if (editor) {
|
||||||
editor.layout();
|
editor.layout();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// 延迟执行,确保编辑器已初始化
|
||||||
|
setTimeout(function() {
|
||||||
|
if (editor) {
|
||||||
|
editor.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 暴露获取内容的方法给QWebEngineView调用
|
// 暴露获取内容的方法给QWebEngineView调用
|
||||||
@@ -151,10 +157,7 @@
|
|||||||
language: editor && editor.getModel() ? editor.getModel().getLanguageId() : 'text'
|
language: editor && editor.getModel() ? editor.getModel().getLanguageId() : 'text'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 方法1: 保存到localStorage
|
// 直接向本地服务器发送POST请求(主要方式)
|
||||||
localStorage.setItem('leonpan_editor_content', JSON.stringify(saveData));
|
|
||||||
|
|
||||||
// 方法2: 直接向本地服务器发送POST请求(主要方式)
|
|
||||||
try {
|
try {
|
||||||
// 获取当前端口号
|
// 获取当前端口号
|
||||||
const currentPort = window.location.port;
|
const currentPort = window.location.port;
|
||||||
@@ -167,53 +170,38 @@
|
|||||||
xhr.onload = function() {
|
xhr.onload = function() {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
console.log('服务器接收保存成功');
|
console.log('服务器接收保存成功');
|
||||||
|
alert('内容已成功保存并返回应用程序');
|
||||||
|
|
||||||
|
// 尝试使用自定义协议返回应用程序
|
||||||
|
try {
|
||||||
|
window.location.href = 'leonpan:save-success';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('无法通过协议返回应用程序:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试关闭窗口(某些浏览器可能阻止此操作)
|
||||||
|
setTimeout(function() {
|
||||||
|
window.close();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
console.error('服务器返回错误状态:', xhr.status);
|
||||||
|
alert('保存失败,请重试');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.onerror = function() {
|
xhr.onerror = function() {
|
||||||
console.error('向服务器发送保存请求失败');
|
console.error('向服务器发送保存请求失败');
|
||||||
|
alert('保存请求发送失败,请检查网络连接');
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.send(JSON.stringify(saveData));
|
xhr.send(JSON.stringify(saveData));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('发送保存请求时出错:', e);
|
console.error('发送保存请求时出错:', e);
|
||||||
|
alert('保存过程中发生错误: ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法3: 创建一个下载链接作为备用方式
|
// 备用: 保存到localStorage,以便应用程序在POST失败时可以尝试读取
|
||||||
const blob = new Blob([JSON.stringify(saveData)], {type: 'application/json'});
|
localStorage.setItem('leonpan_editor_content', JSON.stringify(saveData));
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
|
|
||||||
// 设置文件名和下载路径
|
|
||||||
a.download = 'editor_content.json';
|
|
||||||
|
|
||||||
// 如果提供了临时文件路径,尝试使用该路径
|
|
||||||
if (tempFilePath) {
|
|
||||||
// 对于Windows路径,需要进行特殊处理
|
|
||||||
if (navigator.platform.indexOf('Win') !== -1) {
|
|
||||||
// 在Windows中,我们不能直接设置文件系统路径,但可以提示用户
|
|
||||||
console.log('建议保存路径:', tempFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.href = url;
|
|
||||||
|
|
||||||
// 显示成功提示
|
|
||||||
alert('内容已成功保存!应用程序将自动检测到您的更改。');
|
|
||||||
|
|
||||||
// 自动触发下载(作为备用机制)
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
// 尝试通过协议处理程序返回应用程序
|
|
||||||
try {
|
|
||||||
// 使用自定义协议打开应用程序
|
|
||||||
window.location.href = 'leonpan:save-success';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('无法返回应用程序:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取系统临时目录路径(用于显示给用户)
|
// 获取系统临时目录路径(用于显示给用户)
|
||||||
@@ -228,56 +216,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从URL参数获取初始化数据
|
// 初始化函数(根据URL参数)
|
||||||
function initFromUrl() {
|
function initFromUrl() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const contentParam = urlParams.get('content');
|
||||||
|
const languageParam = urlParams.get('language');
|
||||||
|
|
||||||
// 获取并设置内容
|
if (contentParam) {
|
||||||
const encodedContent = urlParams.get('content');
|
|
||||||
if (encodedContent) {
|
|
||||||
try {
|
try {
|
||||||
const decodedContent = Base.decode(encodedContent);
|
const decodedContent = Base.decode(contentParam);
|
||||||
setEditorContent(decodedContent);
|
setEditorContent(decodedContent);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解码内容失败:', e);
|
console.error('从URL初始化内容失败:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取并设置语言
|
if (languageParam) {
|
||||||
const language = urlParams.get('language') || 'text';
|
setEditorLanguage(languageParam);
|
||||||
setEditorLanguage(language);
|
|
||||||
|
|
||||||
// 保存文件ID到本地存储
|
|
||||||
const fileId = urlParams.get('fileId');
|
|
||||||
if (fileId) {
|
|
||||||
localStorage.setItem('currentFileId', fileId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取临时文件路径
|
|
||||||
tempFilePath = urlParams.get('temp_file') || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化时从URL参数加载数据
|
// 页面加载完成后执行初始化
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
// 从localStorage中加载可能保存的内容
|
|
||||||
const savedContent = localStorage.getItem('leonpan_editor_content');
|
|
||||||
if (savedContent) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(savedContent);
|
|
||||||
if (parsed.saved && parsed.content) {
|
|
||||||
// 延迟执行,确保编辑器已初始化
|
|
||||||
setTimeout(() => {
|
|
||||||
if (editor) {
|
|
||||||
const decodedContent = Base.decode(parsed.content);
|
|
||||||
editor.setValue(decodedContent);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析保存的内容失败:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟执行,确保编辑器已初始化
|
// 延迟执行,确保编辑器已初始化
|
||||||
setTimeout(initFromUrl, 1000);
|
setTimeout(initFromUrl, 1000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ _current_language = "zh"
|
|||||||
# 翻译词典
|
# 翻译词典
|
||||||
_translations = {}
|
_translations = {}
|
||||||
# 语言文件目录
|
# 语言文件目录
|
||||||
_LANG_DIR = Path("app/resource/lang").absolute()
|
_LANG_DIR = Path("_internal/lang").absolute()
|
||||||
# print(Path("app/resource/lang").absolute())
|
# print(Path("app/resource/lang").absolute())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
self._id = _id
|
self._id = _id
|
||||||
self.isChanged = False
|
self.isChanged = False
|
||||||
|
|
||||||
|
# 初始化关键变量
|
||||||
|
self.isContentSaved = False # 明确初始化保存状态
|
||||||
|
self.httpd = None # 服务器实例引用
|
||||||
|
self.server_thread = None # 服务器线程引用
|
||||||
|
self.tempFilePath = None # 临时文件路径
|
||||||
|
|
||||||
# 设置编辑器HTML文件路径 - 修正为项目根目录的_internal文件夹
|
# 设置编辑器HTML文件路径 - 修正为项目根目录的_internal文件夹
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
self.editor_index = os.path.join(project_root, "_internal/editor_main/index.html")
|
self.editor_index = os.path.join(project_root, "_internal/editor_main/index.html")
|
||||||
@@ -348,7 +354,10 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
)
|
)
|
||||||
# 隐藏进度条
|
# 隐藏进度条
|
||||||
self.saveProgressBar.hide()
|
self.saveProgressBar.hide()
|
||||||
QTimer.singleShot(700, self.accept)
|
|
||||||
|
# 直接调用accept()关闭窗口,不再使用定时器延迟
|
||||||
|
# 确保窗口立即关闭并返回成功结果
|
||||||
|
self.accept()
|
||||||
|
|
||||||
def _errorSave(self, msg):
|
def _errorSave(self, msg):
|
||||||
logger.error(f"文本文件保存失败,文件ID: {self._id}, 错误: {msg}")
|
logger.error(f"文本文件保存失败,文件ID: {self._id}, 错误: {msg}")
|
||||||
@@ -432,14 +441,9 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
|
|
||||||
def setTextContent(self, content):
|
def setTextContent(self, content):
|
||||||
"""设置文本内容并在外部浏览器中打开"""
|
"""设置文本内容并在外部浏览器中打开"""
|
||||||
# 检查内容是否已保存,如果已保存则不允许再次编辑
|
# 每次打开文件时都重置isContentSaved状态为False,允许重新编辑
|
||||||
if hasattr(self, 'isContentSaved') and self.isContentSaved:
|
self.isContentSaved = False
|
||||||
logger.warning("内容已保存,不允许再次编辑")
|
logger.info(f"重置isContentSaved为False,文件ID: {self._id}")
|
||||||
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
|
|
||||||
<h3>已经保存</h3>
|
|
||||||
<p>该内容已经保存,不再允许编辑</p>
|
|
||||||
</div>''')
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"文本文件加载成功,原始内容长度: {len(content)}字符")
|
logger.info(f"文本文件加载成功,原始内容长度: {len(content)}字符")
|
||||||
|
|
||||||
@@ -563,10 +567,10 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
attempts = 0
|
attempts = 0
|
||||||
while attempts < max_attempts:
|
while attempts < max_attempts:
|
||||||
try:
|
try:
|
||||||
# 设置处理器的实例引用
|
|
||||||
EditorHTTPRequestHandler.preview_box_instance = self
|
|
||||||
# 使用自定义处理器创建服务器
|
# 使用自定义处理器创建服务器
|
||||||
self.httpd = ReuseTCPServer(("127.0.0.1", port), EditorHTTPRequestHandler)
|
self.httpd = ReuseTCPServer(("127.0.0.1", port), EditorHTTPRequestHandler)
|
||||||
|
# 保存当前实例的引用,以便处理器可以访问
|
||||||
|
self.httpd.RequestHandlerClass.preview_box_instance = self
|
||||||
logger.info(f"Web服务器成功在端口 {port} 启动")
|
logger.info(f"Web服务器成功在端口 {port} 启动")
|
||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -616,87 +620,44 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
self.pollingTimer.start()
|
self.pollingTimer.start()
|
||||||
|
|
||||||
def _checkBrowserContent(self):
|
def _checkBrowserContent(self):
|
||||||
"""检查浏览器是否已保存内容"""
|
"""检查浏览器是否已保存内容 - 现在仅作为备用方案"""
|
||||||
# 首先尝试从localStorage读取
|
# 由于我们使用POST请求直接传递内容,这里不再需要轮询检查文件
|
||||||
try:
|
# 轮询可以保留作为备用方案,但主要保存机制是通过POST请求
|
||||||
import json
|
pass
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# 尝试获取localStorage数据
|
|
||||||
# 在Windows上,localStorage通常存储在用户的AppData目录中
|
|
||||||
# 由于直接访问localStorage有困难,我们使用一个更可靠的方法:
|
|
||||||
# 1. 首先检查临时文件
|
|
||||||
if self.tempFilePath and os.path.exists(self.tempFilePath):
|
|
||||||
try:
|
|
||||||
with open(self.tempFilePath, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
if data.get('saved', False):
|
|
||||||
logger.info("检测到浏览器已保存内容(通过临时文件)")
|
|
||||||
self._processSavedContent(data)
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"读取临时文件失败: {e}")
|
|
||||||
|
|
||||||
# 2. 作为备用方案,检查是否有特定的保存文件
|
|
||||||
# 这个文件可以由浏览器通过特定的方法创建
|
|
||||||
import tempfile
|
|
||||||
app_data_dir = os.path.join(tempfile.gettempdir(), 'LeonPan')
|
|
||||||
os.makedirs(app_data_dir, exist_ok=True)
|
|
||||||
save_file_path = os.path.join(app_data_dir, 'editor_content.json')
|
|
||||||
|
|
||||||
if os.path.exists(save_file_path):
|
|
||||||
try:
|
|
||||||
with open(save_file_path, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
if data.get('saved', False):
|
|
||||||
logger.info("检测到浏览器已保存内容(通过备用文件)")
|
|
||||||
self._processSavedContent(data)
|
|
||||||
# 删除备用文件
|
|
||||||
os.unlink(save_file_path)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"读取备用保存文件失败: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"检查浏览器内容时出错: {e}")
|
|
||||||
|
|
||||||
def _processSavedContent(self, data):
|
def _processSavedContent(self, data):
|
||||||
"""处理已保存的内容"""
|
"""处理已保存的内容"""
|
||||||
# 停止轮询
|
logger.info(f"开始处理保存的内容,文件ID: {self._id}")
|
||||||
self.pollingTimer.stop()
|
|
||||||
|
|
||||||
# 更新内容
|
# 更新内容
|
||||||
import base64
|
import base64
|
||||||
encoded_content = data.get('content', '')
|
encoded_content = data.get('content', '')
|
||||||
try:
|
try:
|
||||||
self.editorContent = base64.b64decode(encoded_content).decode('utf-8')
|
self.editorContent = base64.b64decode(encoded_content).decode('utf-8')
|
||||||
|
logger.info(f"内容解码成功,长度: {len(self.editorContent)}字符")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"解码内容失败: {e}")
|
logger.error(f"解码内容失败: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 标记内容已保存
|
# 不再标记为已保存,让_saveContent方法处理保存逻辑
|
||||||
self.isContentSaved = True
|
# 直接调用_saveContent方法保存内容到服务器,不再重置状态
|
||||||
|
logger.info(f"调用_saveContent方法保存内容到服务器,文件ID: {self._id}")
|
||||||
|
self._saveContent(self.editorContent)
|
||||||
|
|
||||||
# 更新占位符文本,显示已保存信息
|
# 这里不再显示成功信息,因为_saveContent会通过_successSave方法显示
|
||||||
|
# 也不再设置定时器重置状态,因为成功保存后窗口会直接关闭
|
||||||
|
|
||||||
|
def _resetSaveState(self):
|
||||||
|
"""重置保存状态,允许再次编辑和保存"""
|
||||||
|
self.isContentSaved = False
|
||||||
|
logger.info(f"已重置保存状态,允许再次编辑和保存,文件ID: {self._id}")
|
||||||
|
# 恢复默认的占位符文本
|
||||||
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
|
self.placeholderLabel.setText('''<div style='text-align:center; padding:20px;'>
|
||||||
<h3>已经保存</h3>
|
<h3>文本编辑已在外部浏览器中打开</h3>
|
||||||
<p>内容已从浏览器同步到应用并已保存</p>
|
<p><strong>重要提示:</strong>完成编辑后,请点击浏览器中的"保存并返回"按钮</p>
|
||||||
<p>该内容将不再允许编辑</p>
|
<p>应用正在自动检测...</p>
|
||||||
</div>''')
|
</div>''')
|
||||||
|
|
||||||
# 禁用保存按钮
|
|
||||||
self.saveButton.setEnabled(False)
|
|
||||||
|
|
||||||
# 清理临时文件
|
|
||||||
if self.tempFilePath and os.path.exists(self.tempFilePath):
|
|
||||||
try:
|
|
||||||
os.unlink(self.tempFilePath)
|
|
||||||
self.tempFilePath = None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"删除临时文件失败: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def handleError(self, error_msg):
|
def handleError(self, error_msg):
|
||||||
@@ -741,15 +702,10 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
<script>
|
<script>
|
||||||
let editor;
|
let editor;
|
||||||
|
|
||||||
require.config({{ paths: {{ 'vs': './vs' }} }});
|
require.config({ paths: { 'vs': './vs' } });
|
||||||
require(['vs/editor/editor.main'], function() {{
|
require(['vs/editor/editor.main'], function() {{
|
||||||
editor = monaco.editor.create(document.getElementById('container'), {{
|
editor = monaco.editor.create(document.getElementById('container'), {{
|
||||||
value: `{content.replace('`', '\\`')}`,
|
value: `{content.replace('`', '\\`')}`,
|
||||||
}});
|
|
||||||
// 重新获取编辑器实例以确保正确初始化
|
|
||||||
setTimeout(() => {{
|
|
||||||
editor = monaco.editor.getModels()[0] ? monaco.editor.getModels()[0].getContainerInfo().domNode.monacoEditor : null;
|
|
||||||
}}, 100);'\\`'}}`,
|
|
||||||
language: '{language}',
|
language: '{language}',
|
||||||
theme: 'vs-dark',
|
theme: 'vs-dark',
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
@@ -760,7 +716,11 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
fontFamily: 'Consolas, "Microsoft YaHei", monospace'
|
fontFamily: 'Consolas, "Microsoft YaHei", monospace'
|
||||||
}});
|
}});
|
||||||
|
// 重新获取编辑器实例以确保正确初始化
|
||||||
|
setTimeout(() => {{
|
||||||
|
editor = monaco.editor.getModels()[0] ? monaco.editor.getModels()[0].getContainerInfo().domNode.monacoEditor : null;
|
||||||
|
}}, 100);
|
||||||
|
|
||||||
// 添加内容变化监听器
|
// 添加内容变化监听器
|
||||||
editor.onDidChangeModelContent(function() {{
|
editor.onDidChangeModelContent(function() {{
|
||||||
@@ -833,9 +793,40 @@ class PreviewTextBox(MessageBoxBase):
|
|||||||
|
|
||||||
# JavaScript执行完成的处理已移除
|
# JavaScript执行完成的处理已移除
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""析构函数,确保清理资源"""
|
||||||
|
# 停止轮询定时器
|
||||||
|
self.pollingTimer.stop()
|
||||||
|
|
||||||
|
# 停止HTTP服务器
|
||||||
|
if hasattr(self, 'httpd'):
|
||||||
|
try:
|
||||||
|
self.httpd.shutdown()
|
||||||
|
self.httpd.server_close()
|
||||||
|
logger.info("Web服务器已停止")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"停止Web服务器时出错: {e}")
|
||||||
|
|
||||||
|
# 删除临时文件
|
||||||
|
if hasattr(self, 'tempFilePath') and self.tempFilePath and os.path.exists(self.tempFilePath):
|
||||||
|
try:
|
||||||
|
os.unlink(self.tempFilePath)
|
||||||
|
logger.info(f"临时文件已删除: {self.tempFilePath}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除临时文件失败: {e}")
|
||||||
|
|
||||||
def _saveContent(self, content):
|
def _saveContent(self, content):
|
||||||
"""保存编辑器内容并提交修改"""
|
"""保存编辑器内容并提交修改"""
|
||||||
logger.info(f"保存文本文件修改,文件ID: {self._id}")
|
logger.info(f"保存文本文件修改,文件ID: {self._id}")
|
||||||
|
# 确保断开之前可能存在的连接,避免多次连接
|
||||||
|
if hasattr(self, 'saveTextThread') and self.saveTextThread:
|
||||||
|
try:
|
||||||
|
self.saveTextThread.successUpdated.disconnect()
|
||||||
|
self.saveTextThread.errorUpdated.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 创建新的保存线程
|
||||||
self.saveTextThread = UpdateFileContentThread(
|
self.saveTextThread = UpdateFileContentThread(
|
||||||
self._id,
|
self._id,
|
||||||
content,
|
content,
|
||||||
|
|||||||
224
build.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
应用程序构建脚本
|
||||||
|
使用PyInstaller将Python应用打包为可执行文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 项目根目录
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.absolute()
|
||||||
|
|
||||||
|
# 主程序入口
|
||||||
|
MAIN_SCRIPT = "main.py"
|
||||||
|
|
||||||
|
# 输出目录
|
||||||
|
OUTPUT_DIR = PROJECT_ROOT / "dist"
|
||||||
|
BUILD_DIR = PROJECT_ROOT / "build"
|
||||||
|
|
||||||
|
# 应用名称
|
||||||
|
APP_NAME = "LeonPan"
|
||||||
|
APP_NAME_CONSOLE = "LeonPan_Console"
|
||||||
|
|
||||||
|
# 图标文件
|
||||||
|
ICON_FILE = "logo.png"
|
||||||
|
|
||||||
|
# 需要包含的额外文件和目录
|
||||||
|
EXTRA_DATA = [
|
||||||
|
("_internal", "_internal"), # 编辑器和相关资源
|
||||||
|
("logo.png", "logo.png"), # 应用图标
|
||||||
|
("welcome_video.py", "welcome_video.py"), # 欢迎视频脚本
|
||||||
|
("app/resource", "app/resource") # 应用资源
|
||||||
|
]
|
||||||
|
|
||||||
|
def run_command(command, cwd=None):
|
||||||
|
"""执行命令并返回结果"""
|
||||||
|
print(f"执行命令: {' '.join(command)}")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=cwd or PROJECT_ROOT,
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
print(f"命令执行成功: {result.stdout[:100]}..." if len(result.stdout) > 100 else f"命令执行成功: {result.stdout}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"命令执行失败: {e.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
|
"""检查构建依赖是否已安装"""
|
||||||
|
print("检查构建依赖...")
|
||||||
|
|
||||||
|
# 检查pyinstaller
|
||||||
|
try:
|
||||||
|
import PyInstaller
|
||||||
|
print(f"PyInstaller 已安装,版本: {PyInstaller.__version__}")
|
||||||
|
except ImportError:
|
||||||
|
print("PyInstaller 未安装,正在安装...")
|
||||||
|
if not run_command([sys.executable, "-m", "pip", "install", "pyinstaller"]):
|
||||||
|
print("安装PyInstaller失败,请手动安装后重试")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 安装项目依赖
|
||||||
|
requirements_file = PROJECT_ROOT / "requirements.txt"
|
||||||
|
if requirements_file.exists():
|
||||||
|
print(f"安装项目依赖: {requirements_file}")
|
||||||
|
if not run_command([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]):
|
||||||
|
print("安装项目依赖失败,请检查requirements.txt文件")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clean_output():
|
||||||
|
"""清理之前的构建输出"""
|
||||||
|
print("清理之前的构建输出...")
|
||||||
|
|
||||||
|
# 删除dist目录
|
||||||
|
if OUTPUT_DIR.exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(OUTPUT_DIR)
|
||||||
|
print(f"已删除目录: {OUTPUT_DIR}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"删除 {OUTPUT_DIR} 失败: {e}")
|
||||||
|
|
||||||
|
# 删除build目录
|
||||||
|
if BUILD_DIR.exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(BUILD_DIR)
|
||||||
|
print(f"已删除目录: {BUILD_DIR}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"删除 {BUILD_DIR} 失败: {e}")
|
||||||
|
|
||||||
|
# 删除.spec文件
|
||||||
|
spec_file = PROJECT_ROOT / f"{APP_NAME}.spec"
|
||||||
|
if spec_file.exists():
|
||||||
|
try:
|
||||||
|
os.remove(spec_file)
|
||||||
|
print(f"已删除文件: {spec_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"删除 {spec_file} 失败: {e}")
|
||||||
|
|
||||||
|
def build_app():
|
||||||
|
"""使用PyInstaller构建应用"""
|
||||||
|
print("开始构建应用...")
|
||||||
|
|
||||||
|
# 构建两个版本:无控制台版本和有控制台版本
|
||||||
|
builds = [
|
||||||
|
{
|
||||||
|
"name": APP_NAME, # 无控制台版本
|
||||||
|
"console": False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": APP_NAME_CONSOLE, # 有控制台版本
|
||||||
|
"console": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 为每个版本执行构建
|
||||||
|
for build_config in builds:
|
||||||
|
build_name = build_config["name"]
|
||||||
|
use_console = build_config["console"]
|
||||||
|
|
||||||
|
print(f"\n构建版本: {build_name} (控制台: {'启用' if use_console else '禁用'})")
|
||||||
|
|
||||||
|
# 构建pyinstaller命令
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"PyInstaller",
|
||||||
|
MAIN_SCRIPT,
|
||||||
|
"--name", build_name,
|
||||||
|
"--onefile", # 构建单个可执行文件
|
||||||
|
]
|
||||||
|
|
||||||
|
# 设置控制台选项
|
||||||
|
if use_console:
|
||||||
|
cmd.append("--console")
|
||||||
|
else:
|
||||||
|
cmd.extend(["--windowed", "--noconsole"])
|
||||||
|
|
||||||
|
# 添加图标
|
||||||
|
icon_path = PROJECT_ROOT / ICON_FILE
|
||||||
|
if icon_path.exists():
|
||||||
|
cmd.extend(["--icon", str(icon_path)])
|
||||||
|
print(f"使用图标: {icon_path}")
|
||||||
|
else:
|
||||||
|
print(f"警告: 图标文件 {icon_path} 不存在,将使用默认图标")
|
||||||
|
|
||||||
|
# 添加额外的数据文件
|
||||||
|
for src, dst in EXTRA_DATA:
|
||||||
|
src_path = PROJECT_ROOT / src
|
||||||
|
if src_path.exists():
|
||||||
|
cmd.extend(["--add-data", f"{src_path}{os.pathsep}{dst}"])
|
||||||
|
print(f"添加额外数据: {src} -> {dst}")
|
||||||
|
else:
|
||||||
|
print(f"警告: 额外数据 {src_path} 不存在,跳过")
|
||||||
|
|
||||||
|
# 执行构建命令
|
||||||
|
if not run_command(cmd):
|
||||||
|
print(f"{build_name} 构建失败!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("所有版本构建成功!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def copy_additional_files():
|
||||||
|
"""复制额外的文件到输出目录"""
|
||||||
|
print("复制额外文件到输出目录...")
|
||||||
|
|
||||||
|
if not OUTPUT_DIR.exists():
|
||||||
|
print(f"错误: 输出目录 {OUTPUT_DIR} 不存在")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 复制_internal目录(两个可执行文件共用)
|
||||||
|
internal_src = PROJECT_ROOT / "_internal"
|
||||||
|
internal_dst = OUTPUT_DIR / "_internal"
|
||||||
|
if internal_src.exists():
|
||||||
|
try:
|
||||||
|
if internal_dst.exists():
|
||||||
|
shutil.rmtree(internal_dst)
|
||||||
|
shutil.copytree(internal_src, internal_dst)
|
||||||
|
print(f"已复制 _internal 目录到 {internal_dst}(两个可执行文件共用)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"复制 _internal 目录失败: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
print(f"开始构建 {APP_NAME} 应用...")
|
||||||
|
print(f"项目根目录: {PROJECT_ROOT}")
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
if not check_dependencies():
|
||||||
|
print("依赖检查失败,终止构建")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 清理输出
|
||||||
|
clean_output()
|
||||||
|
|
||||||
|
# 构建应用
|
||||||
|
if not build_app():
|
||||||
|
print("应用构建失败,终止")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 复制额外文件
|
||||||
|
copy_additional_files()
|
||||||
|
|
||||||
|
print("\n构建完成!")
|
||||||
|
print(f"无控制台版本可执行文件: {OUTPUT_DIR / APP_NAME}{'.exe' if sys.platform == 'win32' else ''}")
|
||||||
|
print(f"有控制台版本可执行文件: {OUTPUT_DIR / APP_NAME_CONSOLE}{'.exe' if sys.platform == 'win32' else ''}")
|
||||||
|
print(f"共用资源目录: {OUTPUT_DIR / '_internal'}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
BIN
logo.png
|
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 219 KiB |