mirror of
https://github.com/dream-num/univer.git
synced 2026-08-29 07:13:59 +08:00
feat(sheets-drawing-ui): implement batch save images functionality (#6250)
This commit is contained in:
@@ -51,7 +51,6 @@ import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor';
|
||||
import { UniverUIPlugin } from '@univerjs/ui';
|
||||
import { UniverVue3AdapterPlugin } from '@univerjs/ui-adapter-vue3';
|
||||
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component';
|
||||
import { customRangePopups } from './custom/custom-range-popup';
|
||||
import { customRegisterEvent } from './custom/custom-register-event';
|
||||
import { UniverSheetsCustomShortcutPlugin } from './custom/custom-shortcut';
|
||||
import ImportCSVButtonPlugin from './custom/import-csv-button';
|
||||
@@ -202,7 +201,7 @@ function createNewInstance() {
|
||||
// ]);
|
||||
|
||||
customRegisterEvent(univer, window.univerAPI!);
|
||||
customRangePopups(univer, window.univerAPI!);
|
||||
// customRangePopups(univer, window.univerAPI!);
|
||||
}
|
||||
|
||||
createNewInstance();
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/drawing-ui": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "^1.0.2",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-drawing": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import { CommandType, LocaleService } from '@univerjs/core';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
import { IBatchSaveImagesService } from '../../services/batch-save-images.service';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from '../../views/batch-save-images/component-name';
|
||||
|
||||
export const SaveCellImagesCommand: ICommand = {
|
||||
id: 'sheet.command.save-cell-images',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const dialogService = accessor.get(IDialogService);
|
||||
const batchSaveService = accessor.get(IBatchSaveImagesService);
|
||||
|
||||
const images = batchSaveService.getCellImagesInSelection();
|
||||
|
||||
// If only one image, download directly without dialog
|
||||
if (images.length === 1) {
|
||||
try {
|
||||
await batchSaveService.downloadSingleImage(images[0]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to download image:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple images: open batch save dialog
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const selectionRange = batchSaveService.getSelectionRangeNotation();
|
||||
const titleText = `${localeService.t('sheetImage.save.title')} (${selectionRange})`;
|
||||
|
||||
dialogService.open({
|
||||
id: BATCH_SAVE_IMAGES_DIALOG_ID,
|
||||
draggable: true,
|
||||
width: 360,
|
||||
title: { title: titleText },
|
||||
children: {
|
||||
label: BATCH_SAVE_IMAGES_DIALOG_ID,
|
||||
},
|
||||
destroyOnClose: true,
|
||||
preservePositionOnDestroy: true,
|
||||
onClose: () => dialogService.close(BATCH_SAVE_IMAGES_DIALOG_ID),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -15,9 +15,11 @@
|
||||
*/
|
||||
|
||||
import type { MenuSchemaType } from '@univerjs/ui';
|
||||
import { RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { InsertCellImageCommand, InsertFloatImageCommand } from '../commands/commands/insert-image.command';
|
||||
import { SaveCellImagesCommand } from '../commands/commands/save-cell-images.command';
|
||||
import { ImageMenuFactory, SHEETS_IMAGE_MENU_ID, UploadCellImageMenuFactory, UploadFloatImageMenuFactory } from '../views/menu/image.menu';
|
||||
import { SaveCellImagesMenuFactory } from '../views/menu/save-images.menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonInsertGroup.MEDIA]: {
|
||||
@@ -34,4 +36,28 @@ export const menuSchema: MenuSchemaType = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[SaveCellImagesCommand.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: SaveCellImagesMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.COL_HEADER]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[SaveCellImagesCommand.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: SaveCellImagesMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.ROW_HEADER]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[SaveCellImagesCommand.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: SaveCellImagesMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { Disposable, ICommandService, Inject } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DownloadImageIcon } from '@univerjs/icons';
|
||||
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { ComponentManager, IMenuManagerService, IShortcutService } from '@univerjs/ui';
|
||||
@@ -26,12 +27,14 @@ import { InsertSheetDrawingCommand } from '../commands/commands/insert-sheet-dra
|
||||
import { MoveDrawingsCommand } from '../commands/commands/move-drawings.command';
|
||||
|
||||
import { RemoveSheetDrawingCommand } from '../commands/commands/remove-sheet-drawing.command';
|
||||
import { SaveCellImagesCommand } from '../commands/commands/save-cell-images.command';
|
||||
import { SetDrawingArrangeCommand } from '../commands/commands/set-drawing-arrange.command';
|
||||
import { SetSheetDrawingCommand } from '../commands/commands/set-sheet-drawing.command';
|
||||
import { UngroupSheetDrawingCommand } from '../commands/commands/ungroup-sheet-drawing.command';
|
||||
import { ClearSheetDrawingTransformerOperation } from '../commands/operations/clear-drawing-transformer.operation';
|
||||
import { EditSheetDrawingOperation } from '../commands/operations/edit-sheet-drawing.operation';
|
||||
import { SidebarSheetDrawingOperation } from '../commands/operations/open-drawing-panel.operation';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID, BatchSaveImagesDialog } from '../views/batch-save-images';
|
||||
import { COMPONENT_SHEET_DRAWING_PANEL } from '../views/sheet-image-panel/component-name';
|
||||
import { SheetDrawingPanel } from '../views/sheet-image-panel/SheetDrawingPanel';
|
||||
import { menuSchema } from './menu.schema';
|
||||
@@ -54,6 +57,8 @@ export class SheetDrawingUIController extends Disposable {
|
||||
private _initCustomComponents(): void {
|
||||
const componentManager = this._componentManager;
|
||||
this.disposeWithMe(componentManager.register(COMPONENT_SHEET_DRAWING_PANEL, SheetDrawingPanel));
|
||||
this.disposeWithMe(componentManager.register(BATCH_SAVE_IMAGES_DIALOG_ID, BatchSaveImagesDialog));
|
||||
this.disposeWithMe(componentManager.register('DownloadImageIcon', DownloadImageIcon));
|
||||
}
|
||||
|
||||
private _initMenus(): void {
|
||||
@@ -75,6 +80,7 @@ export class SheetDrawingUIController extends Disposable {
|
||||
MoveDrawingsCommand,
|
||||
DeleteDrawingsCommand,
|
||||
SetDrawingArrangeCommand,
|
||||
SaveCellImagesCommand,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export { InsertSheetDrawingCommand } from './commands/commands/insert-sheet-draw
|
||||
export type { IDeleteDrawingCommandParams, IInsertDrawingCommandParams, ISetDrawingCommandParams } from './commands/commands/interfaces';
|
||||
export { MoveDrawingsCommand } from './commands/commands/move-drawings.command';
|
||||
export { RemoveSheetDrawingCommand } from './commands/commands/remove-sheet-drawing.command';
|
||||
export { SaveCellImagesCommand } from './commands/commands/save-cell-images.command';
|
||||
export { SetDrawingArrangeCommand } from './commands/commands/set-drawing-arrange.command';
|
||||
export { SetSheetDrawingCommand } from './commands/commands/set-sheet-drawing.command';
|
||||
export { UngroupSheetDrawingCommand } from './commands/commands/ungroup-sheet-drawing.command';
|
||||
@@ -33,6 +34,7 @@ export { SidebarSheetDrawingOperation } from './commands/operations/open-drawing
|
||||
export type { IUniverSheetsDrawingUIConfig } from './controllers/config.schema';
|
||||
export { SheetDrawingUpdateController } from './controllers/sheet-drawing-update.controller';
|
||||
export { UniverSheetsDrawingUIPlugin } from './plugin';
|
||||
export { calcSheetFloatDomPosition, type ICanvasFloatDom, type ICanvasFloatDomInfo, type IDOMAnchor, SHEET_FLOAT_DOM_PREFIX, SheetCanvasFloatDomManagerService } from './services/canvas-float-dom-manager.service';
|
||||
export { BatchSaveImagesService, FileNamePart, type IBatchSaveImagesConfig, IBatchSaveImagesService, type ICellImageInfo } from './services/batch-save-images.service';
|
||||
|
||||
export { calcSheetFloatDomPosition, type ICanvasFloatDom, type ICanvasFloatDomInfo, type IDOMAnchor, SHEET_FLOAT_DOM_PREFIX, SheetCanvasFloatDomManagerService } from './services/canvas-float-dom-manager.service';
|
||||
export { SHEETS_IMAGE_MENU_ID } from './views/menu/image.menu';
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Edita la imatge',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Desa les imatges de cel·la',
|
||||
menuLabel: 'Desa les imatges de cel·la',
|
||||
imageCount: 'Nombre d\'imatges',
|
||||
fileNameConfig: 'Nom del fitxer',
|
||||
useRowCol: 'Utilitza l\'adreça de la cel·la (A1, B2...)',
|
||||
useColumnValue: 'Utilitza el valor de la columna',
|
||||
selectColumn: 'Selecciona la columna',
|
||||
cancel: 'Cancel·la',
|
||||
confirm: 'Desa',
|
||||
saving: 'Desant...',
|
||||
error: 'No s\'han pogut desar les imatges de cel·la',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'Reemplaça',
|
||||
|
||||
@@ -26,6 +26,20 @@ const locale = {
|
||||
panel: {
|
||||
title: 'Edit Image',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Save Cell Images',
|
||||
menuLabel: 'Save Cell Images',
|
||||
imageCount: 'Image Count',
|
||||
fileNameConfig: 'File Name',
|
||||
useRowCol: 'Use Cell Address (A1, B2...)',
|
||||
useColumnValue: 'Use Column Value',
|
||||
selectColumn: 'Select Column',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Save',
|
||||
saving: 'Saving...',
|
||||
error: 'Failed to save cell images',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'Replace',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Editar imagen',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Guardar imágenes de celda',
|
||||
menuLabel: 'Guardar imágenes de celda',
|
||||
imageCount: 'Cantidad de imágenes',
|
||||
fileNameConfig: 'Nombre del archivo',
|
||||
useRowCol: 'Usar dirección de celda (A1, B2...)',
|
||||
useColumnValue: 'Usar valor de columna',
|
||||
selectColumn: 'Seleccionar columna',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Guardar',
|
||||
saving: 'Guardando...',
|
||||
error: 'Error al guardar las imágenes de celda',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'Reemplazar',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'ویرایش تصویر',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'ذخیره تصاویر سلول',
|
||||
menuLabel: 'ذخیره تصاویر سلول',
|
||||
imageCount: 'تعداد تصاویر',
|
||||
fileNameConfig: 'نام فایل',
|
||||
useRowCol: 'استفاده از آدرس سلول (A1, B2...)',
|
||||
useColumnValue: 'استفاده از مقدار ستون',
|
||||
selectColumn: 'انتخاب ستون',
|
||||
cancel: 'لغو',
|
||||
confirm: 'ذخیره',
|
||||
saving: 'در حال ذخیره...',
|
||||
error: 'ذخیره تصاویر سلول ناموفق بود',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'تعویض',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Modifier l\'image',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Enregistrer les images de cellule',
|
||||
menuLabel: 'Enregistrer les images de cellule',
|
||||
imageCount: 'Nombre d\'images',
|
||||
fileNameConfig: 'Nom du fichier',
|
||||
useRowCol: 'Utiliser l\'adresse de cellule (A1, B2...)',
|
||||
useColumnValue: 'Utiliser la valeur de la colonne',
|
||||
selectColumn: 'Sélectionner la colonne',
|
||||
cancel: 'Annuler',
|
||||
confirm: 'Enregistrer',
|
||||
saving: 'Enregistrement...',
|
||||
error: 'Échec de l\'enregistrement des images de cellule',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'Remplacer',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '画像の編集',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'セル画像を保存',
|
||||
menuLabel: 'セル画像を保存',
|
||||
imageCount: '画像数',
|
||||
fileNameConfig: 'ファイル名',
|
||||
useRowCol: 'セルアドレスを使用 (A1, B2...)',
|
||||
useColumnValue: '列の値を使用',
|
||||
selectColumn: '列を選択',
|
||||
cancel: 'キャンセル',
|
||||
confirm: '保存',
|
||||
saving: '保存中...',
|
||||
error: 'セル画像の保存に失敗しました',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: '画像の変更',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '이미지 편집',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '셀 이미지 저장',
|
||||
menuLabel: '셀 이미지 저장',
|
||||
imageCount: '이미지 수',
|
||||
fileNameConfig: '파일 이름',
|
||||
useRowCol: '셀 주소 사용 (A1, B2...)',
|
||||
useColumnValue: '열 값 사용',
|
||||
selectColumn: '열 선택',
|
||||
cancel: '취소',
|
||||
confirm: '저장',
|
||||
saving: '저장 중...',
|
||||
error: '셀 이미지 저장 실패',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: '바꾸기',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Редактировать изображение',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Сохранить изображения ячеек',
|
||||
menuLabel: 'Сохранить изображения ячеек',
|
||||
imageCount: 'Количество изображений',
|
||||
fileNameConfig: 'Имя файла',
|
||||
useRowCol: 'Использовать адрес ячейки (A1, B2...)',
|
||||
useColumnValue: 'Использовать значение столбца',
|
||||
selectColumn: 'Выбрать столбец',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Сохранить',
|
||||
saving: 'Сохранение...',
|
||||
error: 'Не удалось сохранить изображения ячеек',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'Заменить',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: 'Chỉnh sửa hình ảnh',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Lưu hình ảnh ô',
|
||||
menuLabel: 'Lưu hình ảnh ô',
|
||||
imageCount: 'Số lượng hình ảnh',
|
||||
fileNameConfig: 'Tên tệp',
|
||||
useRowCol: 'Sử dụng địa chỉ ô (A1, B2...)',
|
||||
useColumnValue: 'Sử dụng giá trị cột',
|
||||
selectColumn: 'Chọn cột',
|
||||
cancel: 'Hủy',
|
||||
confirm: 'Lưu',
|
||||
saving: 'Đang lưu...',
|
||||
error: 'Lưu hình ảnh ô thất bại',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: 'Thay thế',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '编辑图片',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '保存单元格图片',
|
||||
menuLabel: '保存单元格图片',
|
||||
imageCount: '图片数量',
|
||||
fileNameConfig: '文件名',
|
||||
useRowCol: '使用单元格地址 (A1, B2...)',
|
||||
useColumnValue: '使用某列的值',
|
||||
selectColumn: '选择列',
|
||||
cancel: '取消',
|
||||
confirm: '保存',
|
||||
saving: '保存中...',
|
||||
error: '保存单元格图片失败',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: '替换',
|
||||
|
||||
@@ -28,6 +28,20 @@ const locale: typeof enUS = {
|
||||
panel: {
|
||||
title: '編圖',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '儲存儲存格圖片',
|
||||
menuLabel: '儲存儲存格圖片',
|
||||
imageCount: '圖片數量',
|
||||
fileNameConfig: '檔案名稱',
|
||||
useRowCol: '使用儲存格地址 (A1, B2...)',
|
||||
useColumnValue: '使用某欄的值',
|
||||
selectColumn: '選擇欄',
|
||||
cancel: '取消',
|
||||
confirm: '儲存',
|
||||
saving: '儲存中...',
|
||||
error: '儲存儲存格圖片失敗',
|
||||
},
|
||||
},
|
||||
'image-popup': {
|
||||
replace: '替換',
|
||||
|
||||
@@ -45,6 +45,7 @@ import { SheetDrawingPrintingController } from './controllers/sheet-drawing-prin
|
||||
import { SheetDrawingTransformAffectedController } from './controllers/sheet-drawing-transform-affected.controller';
|
||||
import { SheetDrawingUpdateController } from './controllers/sheet-drawing-update.controller';
|
||||
import { SheetDrawingUIController } from './controllers/sheet-drawing.controller';
|
||||
import { BatchSaveImagesService, IBatchSaveImagesService } from './services/batch-save-images.service';
|
||||
import { SheetCanvasFloatDomManagerService } from './services/canvas-float-dom-manager.service';
|
||||
|
||||
const PLUGIN_NAME = 'SHEET_IMAGE_UI_PLUGIN';
|
||||
@@ -85,6 +86,7 @@ export class UniverSheetsDrawingUIPlugin extends Plugin {
|
||||
[SheetCellImageController],
|
||||
[SheetCellImageAutofillController],
|
||||
[SheetCellImageCopyPasteController],
|
||||
[IBatchSaveImagesService, { useClass: BatchSaveImagesService }],
|
||||
]);
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICellData, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IImageData } from '@univerjs/drawing';
|
||||
import { createIdentifier, Disposable, IImageIoService, ImageSourceType, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
/**
|
||||
* File name part type for multi-select
|
||||
*/
|
||||
export enum FileNamePart {
|
||||
/**
|
||||
* Use cell address as part of file name (e.g., A1, B2)
|
||||
*/
|
||||
CELL_ADDRESS = 'cellAddress',
|
||||
/**
|
||||
* Use value from a specific column as part of file name
|
||||
*/
|
||||
COLUMN_VALUE = 'columnValue',
|
||||
}
|
||||
|
||||
export interface ICellImageInfo {
|
||||
row: number;
|
||||
col: number;
|
||||
cellAddress: string;
|
||||
source: string;
|
||||
imageSourceType: ImageSourceType;
|
||||
imageId: string;
|
||||
}
|
||||
|
||||
export interface IBatchSaveImagesConfig {
|
||||
/**
|
||||
* Selected file name parts (multi-select)
|
||||
*/
|
||||
fileNameParts: FileNamePart[];
|
||||
/**
|
||||
* Column index for COLUMN_VALUE part
|
||||
*/
|
||||
columnIndex?: number;
|
||||
}
|
||||
|
||||
export interface IBatchSaveImagesService {
|
||||
/**
|
||||
* Get all cell images in the current selection
|
||||
*/
|
||||
getCellImagesInSelection(): ICellImageInfo[];
|
||||
|
||||
/**
|
||||
* Get columns that have data in the current selection
|
||||
*/
|
||||
getDataColumns(): Array<{ index: number; label: string }>;
|
||||
|
||||
/**
|
||||
* Get current selection range as A1 notation
|
||||
*/
|
||||
getSelectionRangeNotation(): string;
|
||||
|
||||
/**
|
||||
* Generate file name for a cell image based on config
|
||||
* @param imageInfo The cell image info
|
||||
* @param config The file name configuration
|
||||
*/
|
||||
generateFileName(imageInfo: ICellImageInfo, config: IBatchSaveImagesConfig): string;
|
||||
|
||||
/**
|
||||
* Save images to the file system
|
||||
* @param images The images to save
|
||||
* @param config The file name configuration
|
||||
*/
|
||||
saveImages(images: ICellImageInfo[], config: IBatchSaveImagesConfig): Promise<void>;
|
||||
|
||||
/**
|
||||
* Download a single image directly
|
||||
* @param imageInfo The cell image info
|
||||
*/
|
||||
downloadSingleImage(imageInfo: ICellImageInfo): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the row range of current selection
|
||||
* Returns the min and max row indices
|
||||
*/
|
||||
getSelectionRowRange(): { startRow: number; endRow: number } | null;
|
||||
|
||||
/**
|
||||
* Get all column indices that are within the current selection
|
||||
*/
|
||||
getSelectionColumnIndices(): Set<number>;
|
||||
}
|
||||
|
||||
export const IBatchSaveImagesService = createIdentifier<IBatchSaveImagesService>('sheets-drawing-ui.batch-save-images.service');
|
||||
|
||||
/**
|
||||
* Convert column index to letter (0 -> A, 1 -> B, etc.)
|
||||
*/
|
||||
function columnIndexToLetter(index: number): string {
|
||||
let letter = '';
|
||||
let temp = index;
|
||||
|
||||
while (temp >= 0) {
|
||||
letter = String.fromCharCode((temp % 26) + 65) + letter;
|
||||
temp = Math.floor(temp / 26) - 1;
|
||||
}
|
||||
|
||||
return letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert row and column to A1 notation
|
||||
*/
|
||||
function toA1Notation(row: number, col: number): string {
|
||||
return `${columnIndexToLetter(col)}${row + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert range to A1 notation
|
||||
*/
|
||||
function rangeToA1Notation(range: IRange): string {
|
||||
const start = toA1Notation(range.startRow, range.startColumn);
|
||||
const end = toA1Notation(range.endRow, range.endColumn);
|
||||
return start === end ? start : `${start}:${end}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cell has image
|
||||
*/
|
||||
function cellHasImage(cell: Nullable<ICellData>): boolean {
|
||||
return !!(cell?.p?.drawingsOrder?.length && cell?.p?.drawingsOrder?.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image data from cell
|
||||
*/
|
||||
function getCellImageData(cell: ICellData): IImageData | null {
|
||||
if (!cell.p?.drawingsOrder?.length || !cell.p?.drawings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawingId = cell.p.drawingsOrder[0];
|
||||
const drawing = cell.p.drawings[drawingId];
|
||||
|
||||
if (!drawing || !('source' in drawing) || !('imageSourceType' in drawing)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return drawing as unknown as IImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension from mime type or source
|
||||
*/
|
||||
function getFileExtension(source: string, imageSourceType: ImageSourceType): string {
|
||||
if (imageSourceType === ImageSourceType.BASE64) {
|
||||
const match = source.match(/^data:image\/(\w+);/);
|
||||
if (match) {
|
||||
return match[1] === 'jpeg' ? 'jpg' : match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get extension from URL
|
||||
if (imageSourceType === ImageSourceType.URL) {
|
||||
const urlMatch = source.match(/\.(\w+)(?:\?|$)/);
|
||||
if (urlMatch) {
|
||||
return urlMatch[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Default to png
|
||||
return 'png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert image source to blob
|
||||
*/
|
||||
async function imageSourceToBlob(source: string, imageSourceType: ImageSourceType): Promise<Blob> {
|
||||
if (imageSourceType === ImageSourceType.BASE64) {
|
||||
const response = await fetch(source);
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
if (imageSourceType === ImageSourceType.URL) {
|
||||
const response = await fetch(source);
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
// For UUID type, we need to get the actual URL from the service
|
||||
throw new Error('UUID image type requires additional handling');
|
||||
}
|
||||
|
||||
export class BatchSaveImagesService extends Disposable implements IBatchSaveImagesService {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionService: SheetsSelectionsService,
|
||||
@IImageIoService private readonly _imageIoService: IImageIoService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getCellImagesInSelection(): ICellImageInfo[] {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return [];
|
||||
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return [];
|
||||
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return [];
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const images: ICellImageInfo[] = [];
|
||||
|
||||
for (const selection of selections) {
|
||||
const { startRow, endRow, startColumn, endColumn } = selection.range;
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
|
||||
if (cellHasImage(cell)) {
|
||||
const imageData = getCellImageData(cell!);
|
||||
if (imageData) {
|
||||
images.push({
|
||||
row,
|
||||
col,
|
||||
cellAddress: toA1Notation(row, col),
|
||||
source: imageData.source,
|
||||
imageSourceType: imageData.imageSourceType,
|
||||
imageId: imageData.drawingId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
getDataColumns(): Array<{ index: number; label: string }> {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return [];
|
||||
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return [];
|
||||
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return [];
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const dataRange = cellMatrix.getDataRange();
|
||||
|
||||
// Get row range and column indices from selection
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
const selectionColumnIndices = new Set<number>();
|
||||
|
||||
for (const selection of selections) {
|
||||
minRow = Math.min(minRow, selection.range.startRow);
|
||||
maxRow = Math.max(maxRow, selection.range.endRow);
|
||||
|
||||
// Collect all column indices within selection
|
||||
for (let col = selection.range.startColumn; col <= selection.range.endColumn; col++) {
|
||||
selectionColumnIndices.add(col);
|
||||
}
|
||||
}
|
||||
|
||||
// Find columns that have values in the selection row range, excluding selection columns
|
||||
const columnsWithData = new Set<number>();
|
||||
|
||||
for (let col = dataRange.startColumn; col <= dataRange.endColumn; col++) {
|
||||
// Skip columns that are within the selection
|
||||
if (selectionColumnIndices.has(col)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let row = minRow; row <= maxRow; row++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
if (cell) {
|
||||
const value = cell.v?.toString() || cell.p?.body?.dataStream?.trim() || '';
|
||||
if (value) {
|
||||
columnsWithData.add(col);
|
||||
break; // Found data in this column, move to next column
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array and sort
|
||||
const columns: Array<{ index: number; label: string }> = [];
|
||||
const sortedCols = Array.from(columnsWithData).sort((a, b) => a - b);
|
||||
|
||||
for (const col of sortedCols) {
|
||||
columns.push({
|
||||
index: col,
|
||||
label: columnIndexToLetter(col),
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
getSelectionRangeNotation(): string {
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return '';
|
||||
|
||||
return selections.map((s) => rangeToA1Notation(s.range)).join(', ');
|
||||
}
|
||||
|
||||
getSelectionRowRange(): { startRow: number; endRow: number } | null {
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return null;
|
||||
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
|
||||
for (const selection of selections) {
|
||||
minRow = Math.min(minRow, selection.range.startRow);
|
||||
maxRow = Math.max(maxRow, selection.range.endRow);
|
||||
}
|
||||
|
||||
return { startRow: minRow, endRow: maxRow };
|
||||
}
|
||||
|
||||
getSelectionColumnIndices(): Set<number> {
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return new Set();
|
||||
|
||||
const columnIndices = new Set<number>();
|
||||
for (const selection of selections) {
|
||||
for (let col = selection.range.startColumn; col <= selection.range.endColumn; col++) {
|
||||
columnIndices.add(col);
|
||||
}
|
||||
}
|
||||
|
||||
return columnIndices;
|
||||
}
|
||||
|
||||
generateFileName(imageInfo: ICellImageInfo, config: IBatchSaveImagesConfig): string {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const extension = getFileExtension(imageInfo.source, imageInfo.imageSourceType);
|
||||
const parts: string[] = [];
|
||||
|
||||
// Process each selected file name part in order
|
||||
for (const part of config.fileNameParts) {
|
||||
if (part === FileNamePart.CELL_ADDRESS) {
|
||||
parts.push(imageInfo.cellAddress);
|
||||
} else if (part === FileNamePart.COLUMN_VALUE && config.columnIndex !== undefined) {
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
if (worksheet) {
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const cell = cellMatrix.getValue(imageInfo.row, config.columnIndex);
|
||||
|
||||
if (cell) {
|
||||
// Get cell display value
|
||||
const value = cell.v?.toString() || cell.p?.body?.dataStream?.trim() || '';
|
||||
if (value) {
|
||||
// Sanitize file name (remove invalid characters)
|
||||
const sanitized = value.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||
if (sanitized) {
|
||||
parts.push(sanitized);
|
||||
}
|
||||
// If sanitized is empty, skip this part (don't add anything)
|
||||
}
|
||||
// If value is empty, skip this part
|
||||
}
|
||||
// If cell is empty, skip this part
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no parts selected or all parts failed, use cell address as default
|
||||
if (parts.length === 0) {
|
||||
return `${imageInfo.cellAddress}.${extension}`;
|
||||
}
|
||||
|
||||
// Join parts with underscore
|
||||
return `${parts.join('_')}.${extension}`;
|
||||
}
|
||||
|
||||
async saveImages(images: ICellImageInfo[], config: IBatchSaveImagesConfig): Promise<void> {
|
||||
// Request directory access using File System Access API
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
const dirHandle = await (window as any).showDirectoryPicker({ mode: 'readwrite' });
|
||||
|
||||
// Track file names to handle duplicates
|
||||
const fileNameCounts = new Map<string, number>();
|
||||
|
||||
for (const imageInfo of images) {
|
||||
let fileName = this.generateFileName(imageInfo, config);
|
||||
|
||||
// Handle duplicate file names
|
||||
const baseName = fileName.replace(/\.\w+$/, '');
|
||||
const ext = fileName.match(/\.\w+$/)?.[0] || '.png';
|
||||
|
||||
const count = fileNameCounts.get(baseName) || 0;
|
||||
if (count > 0) {
|
||||
fileName = `${baseName}_${count}${ext}`;
|
||||
}
|
||||
fileNameCounts.set(baseName, count + 1);
|
||||
|
||||
try {
|
||||
// Get image blob
|
||||
const blob = await this._getImageBlob(imageInfo);
|
||||
|
||||
// Create file and write
|
||||
const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
console.error(`Failed to save image ${fileName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async downloadSingleImage(imageInfo: ICellImageInfo): Promise<void> {
|
||||
const extension = getFileExtension(imageInfo.source, imageInfo.imageSourceType);
|
||||
const fileName = `${imageInfo.cellAddress}.${extension}`;
|
||||
|
||||
try {
|
||||
const blob = await this._getImageBlob(imageInfo);
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error(`Failed to download image ${fileName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getImageBlob(imageInfo: ICellImageInfo): Promise<Blob> {
|
||||
if (imageInfo.imageSourceType === ImageSourceType.UUID) {
|
||||
// For UUID, we need to get the actual image from the service
|
||||
const imageUrl = await this._imageIoService.getImage(imageInfo.source);
|
||||
return imageSourceToBlob(imageUrl, ImageSourceType.URL);
|
||||
}
|
||||
return imageSourceToBlob(imageInfo.source, imageInfo.imageSourceType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { Button, Checkbox, CheckboxGroup, FormLayout, Select } from '@univerjs/design';
|
||||
import { useHighlightRange } from '@univerjs/sheets-ui';
|
||||
import { IDialogService, useDependency } from '@univerjs/ui';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { FileNamePart, IBatchSaveImagesService } from '../../services/batch-save-images.service';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from './component-name';
|
||||
|
||||
export function BatchSaveImagesDialog() {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const dialogService = useDependency(IDialogService);
|
||||
const batchSaveService = useDependency(IBatchSaveImagesService);
|
||||
|
||||
const [fileNameParts, setFileNameParts] = useState<Array<string | number | boolean>>([FileNamePart.CELL_ADDRESS]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const images = useMemo(() => batchSaveService.getCellImagesInSelection(), [batchSaveService]);
|
||||
const dataColumns = useMemo(() => batchSaveService.getDataColumns(), [batchSaveService]);
|
||||
const rowRange = useMemo(() => batchSaveService.getSelectionRowRange(), [batchSaveService]);
|
||||
|
||||
// Check if there are available columns to select (columns outside the selection range)
|
||||
const hasAvailableColumns = dataColumns.length > 0;
|
||||
|
||||
const columnOptions = useMemo(() => {
|
||||
return dataColumns.map((col) => ({
|
||||
label: col.label,
|
||||
value: String(col.index),
|
||||
}));
|
||||
}, [dataColumns]);
|
||||
|
||||
const [selectedColumn, setSelectedColumn] = useState<string>(
|
||||
() => columnOptions.length > 0 ? columnOptions[0].value : '0'
|
||||
);
|
||||
|
||||
// Calculate highlight range based on selected column and original selection row range
|
||||
const highlightRanges = useMemo<IRange[]>(() => {
|
||||
const showColumnSelect = fileNameParts.includes(FileNamePart.COLUMN_VALUE);
|
||||
if (!showColumnSelect || !rowRange) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const colIndex = Number(selectedColumn);
|
||||
return [{
|
||||
startRow: rowRange.startRow,
|
||||
endRow: rowRange.endRow,
|
||||
startColumn: colIndex,
|
||||
endColumn: colIndex,
|
||||
}];
|
||||
}, [fileNameParts, selectedColumn, rowRange]);
|
||||
|
||||
// Highlight the selected column range
|
||||
useHighlightRange(highlightRanges);
|
||||
|
||||
const handleFileNamePartsChange = useCallback((value: Array<string | number | boolean>) => {
|
||||
// Ensure at least one option is selected
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
setFileNameParts(value);
|
||||
}, []);
|
||||
|
||||
const handleColumnChange = useCallback((value: string | number | boolean) => {
|
||||
setSelectedColumn(String(value));
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dialogService.close(BATCH_SAVE_IMAGES_DIALOG_ID);
|
||||
}, [dialogService]);
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await batchSaveService.saveImages(images, {
|
||||
fileNameParts: fileNameParts as FileNamePart[],
|
||||
columnIndex: fileNameParts.includes(FileNamePart.COLUMN_VALUE) ? Number(selectedColumn) : undefined,
|
||||
});
|
||||
|
||||
dialogService.close(BATCH_SAVE_IMAGES_DIALOG_ID);
|
||||
} catch (err) {
|
||||
console.error('Failed to save images:', err);
|
||||
setError(localeService.t('sheetImage.save.error'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [batchSaveService, images, fileNameParts, selectedColumn, dialogService, localeService]);
|
||||
|
||||
const showColumnSelect = fileNameParts.includes(FileNamePart.COLUMN_VALUE);
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-flex-col">
|
||||
<FormLayout label={localeService.t('sheetImage.save.imageCount')}>
|
||||
<div className="univer-text-sm univer-text-gray-600">{images.length}</div>
|
||||
</FormLayout>
|
||||
|
||||
<FormLayout label={localeService.t('sheetImage.save.fileNameConfig')}>
|
||||
<CheckboxGroup value={fileNameParts} onChange={handleFileNamePartsChange} direction="vertical">
|
||||
<Checkbox value={FileNamePart.CELL_ADDRESS} disabled={!hasAvailableColumns}>
|
||||
{localeService.t('sheetImage.save.useRowCol')}
|
||||
</Checkbox>
|
||||
{hasAvailableColumns && (
|
||||
<Checkbox value={FileNamePart.COLUMN_VALUE}>
|
||||
{localeService.t('sheetImage.save.useColumnValue')}
|
||||
</Checkbox>
|
||||
)}
|
||||
</CheckboxGroup>
|
||||
</FormLayout>
|
||||
|
||||
{showColumnSelect && (
|
||||
<FormLayout label={localeService.t('sheetImage.save.selectColumn')}>
|
||||
<Select
|
||||
value={selectedColumn}
|
||||
options={columnOptions}
|
||||
onChange={handleColumnChange}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="univer-text-xs univer-text-red-500">{error}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-justify-end univer-gap-2 univer-border-t univer-border-gray-200 univer-pt-3
|
||||
`}
|
||||
>
|
||||
<Button onClick={handleCancel} disabled={saving}>
|
||||
{localeService.t('sheetImage.save.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={saving || images.length === 0}
|
||||
>
|
||||
{saving ? localeService.t('sheetImage.save.saving') : localeService.t('sheetImage.save.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const BATCH_SAVE_IMAGES_DIALOG_ID = 'sheet.dialog.batch-save-images';
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BatchSaveImagesDialog } from './BatchSaveImagesDialog';
|
||||
export { BATCH_SAVE_IMAGES_DIALOG_ID } from './component-name';
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IAccessor, ICellData, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IMenuItem } from '@univerjs/ui';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { combineLatest, map, of, switchMap } from 'rxjs';
|
||||
import { SaveCellImagesCommand } from '../../commands/commands/save-cell-images.command';
|
||||
|
||||
export const SAVE_CELL_IMAGES_MENU_ID = 'sheet.menu.save-cell-images';
|
||||
|
||||
/**
|
||||
* Check if a cell has image
|
||||
*/
|
||||
function cellHasImage(cell: Nullable<ICellData>): boolean {
|
||||
return !!(cell?.p?.drawingsOrder?.length && cell?.p?.drawingsOrder?.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if selection range has any images
|
||||
*/
|
||||
function selectionHasImages(
|
||||
workbook: Workbook,
|
||||
selection: IRange
|
||||
): boolean {
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return false;
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const { startRow, endRow, startColumn, endColumn } = selection;
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
if (cellHasImage(cell)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if File System Access API is supported
|
||||
*/
|
||||
function isFileSystemAccessSupported(): boolean {
|
||||
return 'showDirectoryPicker' in window;
|
||||
}
|
||||
|
||||
export function SaveCellImagesMenuFactory(accessor: IAccessor): IMenuItem {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selectionService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
// Hide menu if File System Access API is not supported or no images in selection
|
||||
const hidden$ = combineLatest([
|
||||
getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET).pipe(
|
||||
switchMap((workbook) => {
|
||||
if (!workbook) return of(true);
|
||||
|
||||
return selectionService.selectionMoveEnd$.pipe(
|
||||
map(() => {
|
||||
// Hide if File System Access API is not supported
|
||||
if (!isFileSystemAccessSupported()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selections = selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any selection has images
|
||||
for (const selection of selections) {
|
||||
if (selectionHasImages(workbook, selection.range)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
})
|
||||
),
|
||||
]).pipe(
|
||||
map(([hidden, noImages]) => hidden || noImages)
|
||||
);
|
||||
|
||||
return {
|
||||
id: SaveCellImagesCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'DownloadImageIcon',
|
||||
title: 'sheetImage.save.menuLabel',
|
||||
hidden$,
|
||||
};
|
||||
}
|
||||
@@ -30,16 +30,7 @@ export const useHighlightRange = (ranges: IRange[] = []) => {
|
||||
stroke: '#49B811',
|
||||
widgets: {},
|
||||
},
|
||||
primary: {
|
||||
startColumn: range.startColumn,
|
||||
endColumn: range.endColumn,
|
||||
startRow: range.startRow,
|
||||
endRow: range.endRow,
|
||||
actualRow: range.startRow,
|
||||
actualColumn: range.startColumn,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
primary: null,
|
||||
}));
|
||||
return () => {
|
||||
ids.forEach((id) => {
|
||||
|
||||
Generated
+3
@@ -2249,6 +2249,9 @@ importers:
|
||||
'@univerjs/engine-render':
|
||||
specifier: workspace:*
|
||||
version: link:../engine-render
|
||||
'@univerjs/icons':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@univerjs/sheets':
|
||||
specifier: workspace:*
|
||||
version: link:../sheets
|
||||
|
||||
Reference in New Issue
Block a user