@sequent-org/moodboard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- package/src/utils/topbarIconLoader.js +114 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { GridFactory } from '../grid/GridFactory.js';
|
|
2
|
+
import { Events } from '../core/events/Events.js';
|
|
3
|
+
|
|
4
|
+
export class BoardService {
|
|
5
|
+
constructor(eventBus, pixi) {
|
|
6
|
+
this.eventBus = eventBus;
|
|
7
|
+
this.pixi = pixi;
|
|
8
|
+
this.grid = null;
|
|
9
|
+
this._getCanvasSize = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async init(getCanvasSize) {
|
|
13
|
+
this._getCanvasSize = getCanvasSize;
|
|
14
|
+
// Инициализируем сетку (по умолчанию линейная)
|
|
15
|
+
const canvasSize = (this._getCanvasSize?.() || {});
|
|
16
|
+
this.grid = GridFactory.createGrid('line', {
|
|
17
|
+
enabled: true,
|
|
18
|
+
size: 20,
|
|
19
|
+
width: canvasSize.width || 800,
|
|
20
|
+
height: canvasSize.height || 600,
|
|
21
|
+
color: 0xE6E6E6,
|
|
22
|
+
opacity: 0.5
|
|
23
|
+
});
|
|
24
|
+
this.grid.updateVisual();
|
|
25
|
+
this.pixi.setGrid(this.grid);
|
|
26
|
+
this.eventBus.emit(Events.UI.GridCurrent, { type: 'line' });
|
|
27
|
+
|
|
28
|
+
this._attachEvents();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_attachEvents() {
|
|
32
|
+
// Смена вида сетки из UI
|
|
33
|
+
this.eventBus.on(Events.UI.GridChange, ({ type }) => {
|
|
34
|
+
const size = this._getCanvasSize?.() || { width: 800, height: 600 };
|
|
35
|
+
if (type === 'off') {
|
|
36
|
+
this.grid?.setEnabled(false);
|
|
37
|
+
this.grid?.updateVisual();
|
|
38
|
+
this.pixi.setGrid(this.grid);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const options = {
|
|
42
|
+
...GridFactory.getDefaultOptions(type),
|
|
43
|
+
enabled: true,
|
|
44
|
+
width: size.width,
|
|
45
|
+
height: size.height
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
this.grid = GridFactory.createGrid(type, options);
|
|
49
|
+
this.grid.updateVisual();
|
|
50
|
+
this.pixi.setGrid(this.grid);
|
|
51
|
+
this.eventBus.emit(Events.UI.GridCurrent, { type });
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.warn('Unknown grid type:', type);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Миникарта: данные и управление
|
|
58
|
+
this.eventBus.on(Events.UI.MinimapGetData, (req) => {
|
|
59
|
+
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
60
|
+
const viewEl = this.pixi.app.view;
|
|
61
|
+
const objects = (this.pixi?.objects ? Array.from(this.pixi.objects.keys()) : []).map((id) => id);
|
|
62
|
+
req.world = { x: world.x, y: world.y, scale: world.scale?.x || 1 };
|
|
63
|
+
req.view = { width: viewEl.clientWidth, height: viewEl.clientHeight };
|
|
64
|
+
// Прокидываем только метаданные объектов через ядро (сам список формирует Core)
|
|
65
|
+
});
|
|
66
|
+
this.eventBus.on(Events.UI.MinimapCenterOn, ({ worldX, worldY }) => {
|
|
67
|
+
const world = this.pixi.worldLayer || this.pixi.app.stage;
|
|
68
|
+
const viewW = this.pixi.app.view.clientWidth;
|
|
69
|
+
const viewH = this.pixi.app.view.clientHeight;
|
|
70
|
+
const s = world.scale?.x || 1;
|
|
71
|
+
world.x = viewW / 2 - worldX * s;
|
|
72
|
+
world.y = viewH / 2 - worldY * s;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resize() {
|
|
77
|
+
if (!this.grid) return;
|
|
78
|
+
const size = this._getCanvasSize?.() || { width: 800, height: 600 };
|
|
79
|
+
this.grid.resize(size.width, size.height);
|
|
80
|
+
this.grid.updateVisual();
|
|
81
|
+
this.pixi.setGrid(this.grid);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сервис для загрузки и управления файлами на сервере
|
|
3
|
+
*/
|
|
4
|
+
export class FileUploadService {
|
|
5
|
+
constructor(apiClient) {
|
|
6
|
+
this.apiClient = apiClient;
|
|
7
|
+
this.uploadEndpoint = '/api/files/upload';
|
|
8
|
+
this.deleteEndpoint = '/api/files';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Загружает файл на сервер
|
|
13
|
+
* @param {File|Blob} file - файл
|
|
14
|
+
* @param {string} name - имя файла
|
|
15
|
+
* @returns {Promise<{id: string, url: string, size: number, mimeType: string, formattedSize: string}>}
|
|
16
|
+
*/
|
|
17
|
+
async uploadFile(file, name = null) {
|
|
18
|
+
try {
|
|
19
|
+
// Создаем FormData для отправки файла
|
|
20
|
+
const formData = new FormData();
|
|
21
|
+
formData.append('file', file);
|
|
22
|
+
formData.append('name', name || file.name || 'file');
|
|
23
|
+
|
|
24
|
+
// Получаем CSRF токен
|
|
25
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
26
|
+
|
|
27
|
+
if (!csrfToken) {
|
|
28
|
+
throw new Error('CSRF токен не найден');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const response = await fetch(this.uploadEndpoint, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
35
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
36
|
+
},
|
|
37
|
+
credentials: 'same-origin',
|
|
38
|
+
body: formData
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const errorData = await response.json().catch(() => null);
|
|
43
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await response.json();
|
|
47
|
+
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
throw new Error(result.message || 'Ошибка загрузки файла');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: result.data.id,
|
|
54
|
+
url: result.data.url,
|
|
55
|
+
size: result.data.size,
|
|
56
|
+
mimeType: result.data.mime_type,
|
|
57
|
+
formattedSize: result.data.formatted_size,
|
|
58
|
+
name: result.data.name
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Ошибка загрузки файла:', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Обновляет метаданные файла на сервере
|
|
69
|
+
* @param {string} fileId - ID файла
|
|
70
|
+
* @param {Object} metadata - метаданные для обновления
|
|
71
|
+
* @returns {Promise<Object>}
|
|
72
|
+
*/
|
|
73
|
+
async updateFileMetadata(fileId, metadata) {
|
|
74
|
+
try {
|
|
75
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
76
|
+
|
|
77
|
+
if (!csrfToken) {
|
|
78
|
+
throw new Error('CSRF токен не найден');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
82
|
+
method: 'PUT',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
86
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
87
|
+
'Accept': 'application/json'
|
|
88
|
+
},
|
|
89
|
+
credentials: 'same-origin',
|
|
90
|
+
body: JSON.stringify(metadata)
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorData = await response.json().catch(() => null);
|
|
95
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await response.json();
|
|
99
|
+
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
throw new Error(result.message || 'Ошибка обновления метаданных файла');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result.data;
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Ошибка обновления метаданных файла:', error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Удаляет файл с сервера
|
|
114
|
+
* @param {string} fileId - ID файла
|
|
115
|
+
*/
|
|
116
|
+
async deleteFile(fileId) {
|
|
117
|
+
try {
|
|
118
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
119
|
+
|
|
120
|
+
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
headers: {
|
|
123
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
124
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
125
|
+
'Accept': 'application/json'
|
|
126
|
+
},
|
|
127
|
+
credentials: 'same-origin'
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const errorData = await response.json().catch(() => null);
|
|
132
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await response.json();
|
|
136
|
+
return result.success;
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Ошибка удаления файла:', error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Получает информацию о файле
|
|
146
|
+
* @param {string} fileId - ID файла
|
|
147
|
+
*/
|
|
148
|
+
async getFileInfo(fileId) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
151
|
+
method: 'GET',
|
|
152
|
+
headers: {
|
|
153
|
+
'Accept': 'application/json',
|
|
154
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
155
|
+
},
|
|
156
|
+
credentials: 'same-origin'
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = await response.json();
|
|
164
|
+
return result.data;
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Ошибка получения информации о файле:', error);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Получает URL для скачивания файла
|
|
174
|
+
* @param {string} fileId - ID файла
|
|
175
|
+
*/
|
|
176
|
+
getDownloadUrl(fileId) {
|
|
177
|
+
return `${this.deleteEndpoint}/${fileId}/download`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Скачивает файл с сервера
|
|
182
|
+
* @param {string} fileId - ID файла
|
|
183
|
+
* @param {string} fileName - имя файла для скачивания
|
|
184
|
+
*/
|
|
185
|
+
async downloadFile(fileId, fileName = null) {
|
|
186
|
+
try {
|
|
187
|
+
const downloadUrl = this.getDownloadUrl(fileId);
|
|
188
|
+
|
|
189
|
+
console.log('📥 FileUploadService: Начинаем скачивание файла:', {
|
|
190
|
+
fileId,
|
|
191
|
+
fileName,
|
|
192
|
+
downloadUrl,
|
|
193
|
+
userAgent: navigator.userAgent,
|
|
194
|
+
isSecureContext: window.isSecureContext
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Метод 1: Попробуем через fetch + blob (более надежно для современных браузеров)
|
|
198
|
+
try {
|
|
199
|
+
console.log('🔄 Метод 1: Пробуем скачивание через fetch...');
|
|
200
|
+
|
|
201
|
+
const response = await fetch(downloadUrl, {
|
|
202
|
+
method: 'GET',
|
|
203
|
+
headers: {
|
|
204
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
205
|
+
},
|
|
206
|
+
credentials: 'same-origin'
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
console.log('📥 Ответ сервера:', {
|
|
210
|
+
status: response.status,
|
|
211
|
+
statusText: response.statusText,
|
|
212
|
+
contentType: response.headers.get('content-type'),
|
|
213
|
+
contentLength: response.headers.get('content-length'),
|
|
214
|
+
contentDisposition: response.headers.get('content-disposition')
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
// Пытаемся получить JSON ошибку от Laravel
|
|
219
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const errorData = await response.json();
|
|
223
|
+
console.error('🚨 Ошибка от сервера:', errorData);
|
|
224
|
+
|
|
225
|
+
if (errorData.message) {
|
|
226
|
+
errorMessage = `${errorMessage} - ${errorData.message}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Показываем пользователю детальную ошибку
|
|
230
|
+
if (errorData.success === false) {
|
|
231
|
+
alert(`Ошибка сервера: ${errorData.message || 'Файл не найден'}`);
|
|
232
|
+
}
|
|
233
|
+
} catch (jsonError) {
|
|
234
|
+
console.warn('Не удалось прочитать JSON ошибку:', jsonError);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new Error(errorMessage);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Получаем blob файла
|
|
241
|
+
const blob = await response.blob();
|
|
242
|
+
console.log('📦 Получен blob:', {
|
|
243
|
+
size: blob.size,
|
|
244
|
+
type: blob.type
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Создаем URL для blob
|
|
248
|
+
const blobUrl = window.URL.createObjectURL(blob);
|
|
249
|
+
|
|
250
|
+
// Создаем ссылку для скачивания
|
|
251
|
+
const link = document.createElement('a');
|
|
252
|
+
link.href = blobUrl;
|
|
253
|
+
link.download = fileName || `file_${fileId}`;
|
|
254
|
+
|
|
255
|
+
// Добавляем в DOM, кликаем и удаляем
|
|
256
|
+
document.body.appendChild(link);
|
|
257
|
+
link.click();
|
|
258
|
+
document.body.removeChild(link);
|
|
259
|
+
|
|
260
|
+
// Освобождаем память
|
|
261
|
+
window.URL.revokeObjectURL(blobUrl);
|
|
262
|
+
|
|
263
|
+
console.log('✅ Файл успешно скачан через fetch/blob:', fileName || fileId);
|
|
264
|
+
return true;
|
|
265
|
+
|
|
266
|
+
} catch (fetchError) {
|
|
267
|
+
console.warn('❌ Ошибка скачивания через fetch:', fetchError);
|
|
268
|
+
|
|
269
|
+
// Метод 2: Fallback - открываем в новом окне
|
|
270
|
+
console.log('🔄 Метод 2: Пробуем открытие в новом окне...');
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const newWindow = window.open(downloadUrl, '_blank');
|
|
274
|
+
if (newWindow) {
|
|
275
|
+
console.log('✅ Файл открыт в новом окне');
|
|
276
|
+
return true;
|
|
277
|
+
} else {
|
|
278
|
+
throw new Error('Popup заблокирован браузером');
|
|
279
|
+
}
|
|
280
|
+
} catch (windowError) {
|
|
281
|
+
console.warn('❌ Ошибка открытия в новом окне:', windowError);
|
|
282
|
+
|
|
283
|
+
// Метод 3: Последний fallback - прямая ссылка
|
|
284
|
+
console.log('🔄 Метод 3: Создаем прямую ссылку...');
|
|
285
|
+
|
|
286
|
+
const link = document.createElement('a');
|
|
287
|
+
link.href = downloadUrl;
|
|
288
|
+
if (fileName) {
|
|
289
|
+
link.download = fileName;
|
|
290
|
+
}
|
|
291
|
+
link.target = '_blank';
|
|
292
|
+
|
|
293
|
+
document.body.appendChild(link);
|
|
294
|
+
link.click();
|
|
295
|
+
document.body.removeChild(link);
|
|
296
|
+
|
|
297
|
+
console.log('✅ Создана прямая ссылка для скачивания');
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('❌ FileUploadService: Критическая ошибка скачивания файла:', error);
|
|
304
|
+
|
|
305
|
+
// Показываем пользователю альтернативную ссылку
|
|
306
|
+
if (confirm(`Автоматическое скачивание не удалось: ${error.message}\n\nОткрыть файл в новой вкладке?`)) {
|
|
307
|
+
window.open(this.getDownloadUrl(fileId), '_blank');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Очищает неиспользуемые файлы с сервера
|
|
316
|
+
* @returns {Promise<{deletedCount: number, errors: Array}>}
|
|
317
|
+
*/
|
|
318
|
+
async cleanupUnusedFiles() {
|
|
319
|
+
try {
|
|
320
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
321
|
+
|
|
322
|
+
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
326
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
327
|
+
'Accept': 'application/json'
|
|
328
|
+
},
|
|
329
|
+
credentials: 'same-origin'
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
const errorData = await response.json().catch(() => null);
|
|
334
|
+
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const result = await response.json();
|
|
338
|
+
|
|
339
|
+
if (result.success) {
|
|
340
|
+
const data = result.data || {};
|
|
341
|
+
return {
|
|
342
|
+
deletedCount: data.deleted_count || 0,
|
|
343
|
+
errors: data.errors || []
|
|
344
|
+
};
|
|
345
|
+
} else {
|
|
346
|
+
throw new Error(result.message || 'Ошибка очистки файлов');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Ошибка очистки неиспользуемых файлов:', error);
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Проверяет, поддерживается ли тип файла для предварительного просмотра
|
|
357
|
+
*/
|
|
358
|
+
static canPreview(mimeType) {
|
|
359
|
+
const previewableTypes = [
|
|
360
|
+
'image/',
|
|
361
|
+
'text/',
|
|
362
|
+
'application/pdf',
|
|
363
|
+
'application/json'
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
return previewableTypes.some(type => mimeType.startsWith(type));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Получает иконку файла по MIME типу
|
|
371
|
+
*/
|
|
372
|
+
static getFileIcon(mimeType) {
|
|
373
|
+
const iconMap = {
|
|
374
|
+
'application/pdf': 'pdf',
|
|
375
|
+
'application/msword': 'doc',
|
|
376
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'doc',
|
|
377
|
+
'application/vnd.ms-excel': 'xls',
|
|
378
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xls',
|
|
379
|
+
'application/vnd.ms-powerpoint': 'ppt',
|
|
380
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'ppt',
|
|
381
|
+
'application/zip': 'archive',
|
|
382
|
+
'application/x-rar-compressed': 'archive',
|
|
383
|
+
'application/x-7z-compressed': 'archive',
|
|
384
|
+
'text/': 'text',
|
|
385
|
+
'image/': 'image',
|
|
386
|
+
'video/': 'video',
|
|
387
|
+
'audio/': 'audio'
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
for (const [type, icon] of Object.entries(iconMap)) {
|
|
391
|
+
if (mimeType.startsWith(type)) {
|
|
392
|
+
return icon;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return 'file';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
export class FrameService {
|
|
4
|
+
constructor(eventBus, pixi, state) {
|
|
5
|
+
this.eventBus = eventBus;
|
|
6
|
+
this.pixi = pixi;
|
|
7
|
+
this.state = state;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
attach() {
|
|
11
|
+
// Визуал подсветки при drag над фреймом и перенос детей на drag
|
|
12
|
+
this.eventBus.on(Events.Tool.DragStart, (data) => {
|
|
13
|
+
const moved = this.state.state.objects.find(o => o.id === data.object);
|
|
14
|
+
if (moved && moved.type === 'frame') {
|
|
15
|
+
// Серый фон
|
|
16
|
+
this.pixi.setFrameFill(moved.id, moved.width, moved.height, 0xEEEEEE);
|
|
17
|
+
// Cнимок стартовых позиций
|
|
18
|
+
this._frameDragFrameStart = { x: this.pixi.objects.get(moved.id)?.x || 0, y: this.pixi.objects.get(moved.id)?.y || 0 };
|
|
19
|
+
const attachments = this._getFrameChildren(moved.id);
|
|
20
|
+
this._frameDragChildStart = new Map();
|
|
21
|
+
for (const childId of attachments) {
|
|
22
|
+
const childPixi = this.pixi.objects.get(childId);
|
|
23
|
+
if (childPixi) this._frameDragChildStart.set(childId, { x: childPixi.x, y: childPixi.y });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.eventBus.on(Events.Tool.DragUpdate, (data) => {
|
|
29
|
+
const moved = this.state.state.objects.find(o => o.id === data.object);
|
|
30
|
+
if (!moved) return;
|
|
31
|
+
if (moved.type === 'frame') {
|
|
32
|
+
const attachments = this._getFrameChildren(moved.id);
|
|
33
|
+
const frameStart = this._frameDragFrameStart || { x: data.position.x, y: data.position.y };
|
|
34
|
+
const dx = data.position.x - frameStart.x;
|
|
35
|
+
const dy = data.position.y - frameStart.y;
|
|
36
|
+
for (const childId of attachments) {
|
|
37
|
+
const start = this._frameDragChildStart?.get(childId);
|
|
38
|
+
if (!start) continue;
|
|
39
|
+
const p = this.pixi.objects.get(childId);
|
|
40
|
+
if (p) { p.x = start.x + dx; p.y = start.y + dy; }
|
|
41
|
+
const stObj = this.state.state.objects.find(o => o.id === childId);
|
|
42
|
+
if (stObj) { stObj.position.x = start.x + dx; stObj.position.y = start.y + dy; }
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
// Hover-эффект: подсветка фрейма, если центр объекта внутри
|
|
46
|
+
const centerX = moved.position.x + (moved.width || 0) / 2;
|
|
47
|
+
const centerY = moved.position.y + (moved.height || 0) / 2;
|
|
48
|
+
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
49
|
+
const ordered = frames.slice().sort((a, b) => {
|
|
50
|
+
const pa = this.pixi.objects.get(a.id);
|
|
51
|
+
const pb = this.pixi.objects.get(b.id);
|
|
52
|
+
return (pb?.zIndex || 0) - (pa?.zIndex || 0);
|
|
53
|
+
});
|
|
54
|
+
let hoverId = null;
|
|
55
|
+
for (const f of ordered) {
|
|
56
|
+
const rect = { x: f.position.x, y: f.position.y, w: f.width || 0, h: f.height || 0 };
|
|
57
|
+
if (centerX >= rect.x && centerX <= rect.x + rect.w && centerY >= rect.y && centerY <= rect.y + rect.h) {
|
|
58
|
+
hoverId = f.id; break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (hoverId !== this._frameHoverId) {
|
|
62
|
+
// Снять подсветку с предыдущего
|
|
63
|
+
if (this._frameHoverId) {
|
|
64
|
+
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
65
|
+
if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
|
|
66
|
+
}
|
|
67
|
+
// Включить подсветку нового
|
|
68
|
+
if (hoverId) {
|
|
69
|
+
const cur = frames.find(fr => fr.id === hoverId);
|
|
70
|
+
if (cur) this.pixi.setFrameFill(cur.id, cur.width, cur.height, 0xEEEEEE);
|
|
71
|
+
}
|
|
72
|
+
this._frameHoverId = hoverId || null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.eventBus.on(Events.Tool.DragEnd, (data) => {
|
|
78
|
+
const movedObj = this.state.state.objects.find(o => o.id === data.object);
|
|
79
|
+
if (!movedObj) return;
|
|
80
|
+
// Сброс заливки
|
|
81
|
+
if (movedObj.type === 'frame') {
|
|
82
|
+
this.pixi.setFrameFill(movedObj.id, movedObj.width, movedObj.height, 0xFFFFFF);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Автопривязка/отвязка объекта к фрейму после перемещения
|
|
86
|
+
this._recomputeFrameAttachment(movedObj.id);
|
|
87
|
+
// Сброс временных структур и hover-подсветки
|
|
88
|
+
this._frameDragFrameStart = null;
|
|
89
|
+
this._frameDragChildStart = null;
|
|
90
|
+
if (this._frameHoverId) {
|
|
91
|
+
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
92
|
+
const prev = frames.find(fr => fr.id === this._frameHoverId);
|
|
93
|
+
if (prev) this.pixi.setFrameFill(prev.id, prev.width, prev.height, 0xFFFFFF);
|
|
94
|
+
this._frameHoverId = null;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_getFrameChildren(frameId) {
|
|
100
|
+
const res = [];
|
|
101
|
+
for (const o of this.state.state.objects || []) {
|
|
102
|
+
if (o.id === frameId) continue;
|
|
103
|
+
if (o.properties && o.properties.frameId === frameId) res.push(o.id);
|
|
104
|
+
}
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_recomputeFrameAttachment(objectId) {
|
|
109
|
+
const obj = (this.state.state.objects || []).find(o => o.id === objectId);
|
|
110
|
+
if (!obj) return;
|
|
111
|
+
if (obj.type === 'frame') return; // фрейм к фрейму не крепим
|
|
112
|
+
const center = {
|
|
113
|
+
x: obj.position.x + (obj.width || 0) / 2,
|
|
114
|
+
y: obj.position.y + (obj.height || 0) / 2
|
|
115
|
+
};
|
|
116
|
+
const frames = (this.state.state.objects || []).filter(o => o.type === 'frame');
|
|
117
|
+
const ordered = frames.slice().sort((a, b) => {
|
|
118
|
+
const pa = this.pixi.objects.get(a.id);
|
|
119
|
+
const pb = this.pixi.objects.get(b.id);
|
|
120
|
+
return (pb?.zIndex || 0) - (pa?.zIndex || 0);
|
|
121
|
+
});
|
|
122
|
+
let newFrameId = null;
|
|
123
|
+
for (const f of ordered) {
|
|
124
|
+
const rect = { x: f.position.x, y: f.position.y, w: f.width || 0, h: f.height || 0 };
|
|
125
|
+
if (center.x >= rect.x && center.x <= rect.x + rect.w && center.y >= rect.y && center.y <= rect.y + rect.h) {
|
|
126
|
+
newFrameId = f.id; break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const prevFrameId = obj.properties?.frameId || null;
|
|
130
|
+
if (newFrameId !== prevFrameId) {
|
|
131
|
+
obj.properties = obj.properties || {};
|
|
132
|
+
obj.properties.frameId = newFrameId || undefined;
|
|
133
|
+
this.state.markDirty();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|