diff --git a/electron-projects/ucap-webmessenger-electron/src/app/AppWindow.ts b/electron-projects/ucap-webmessenger-electron/src/app/AppWindow.ts index 989b3c69..f5502769 100644 --- a/electron-projects/ucap-webmessenger-electron/src/app/AppWindow.ts +++ b/electron-projects/ucap-webmessenger-electron/src/app/AppWindow.ts @@ -23,8 +23,8 @@ export class AppWindow { // tslint:disable-next-line: variable-name private _rendererReadyTime: number | null = null; - private minWidth = 700; - private minHeight = 600; + private minWidth = 1160; + private minHeight = 800; public constructor(private appIconPath: string) { const savedWindowState = windowStateKeeper({ @@ -165,7 +165,11 @@ export class AppWindow { } else { this.window.loadURL( url.format({ - pathname: path.join(__dirname, '..', 'ucap-webmessenger-app/index.html'), + pathname: path.join( + __dirname, + '..', + 'ucap-webmessenger-app/index.html' + ), protocol: 'file:', slashes: true }) diff --git a/electron-projects/ucap-webmessenger-electron/src/index.ts b/electron-projects/ucap-webmessenger-electron/src/index.ts index 6187ec73..b8d3935b 100644 --- a/electron-projects/ucap-webmessenger-electron/src/index.ts +++ b/electron-projects/ucap-webmessenger-electron/src/index.ts @@ -6,7 +6,8 @@ import { Menu, shell, dialog, - BrowserWindow + BrowserWindow, + clipboard } from 'electron'; import path from 'path'; import fse from 'fs-extra'; @@ -29,7 +30,8 @@ import { ChatChannel, MessengerChannel, MessageChannel, - AppChannel + AppChannel, + ClipboardChannel } from '@ucap-webmessenger/native-electron'; import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification'; import { ElectronUpdateWindowService } from '@ucap-webmessenger/electron-update-window'; @@ -729,6 +731,25 @@ ipcMain.on( } ); +ipcMain.on(ClipboardChannel.Read, (event: IpcMainEvent, ...args: any[]) => { + try { + const text = clipboard.readText('clipboard'); + const rtf = clipboard.readRTF('clipboard'); + const html = clipboard.readHTML('clipboard'); + const image = clipboard.readImage('clipboard'); + + event.returnValue = { + text, + rtf, + html, + image: !image.isEmpty() ? image.toBitmap() : undefined, + imageDataUrl: !image.isEmpty() ? image.toDataURL() : undefined + }; + } catch (error) { + event.returnValue = {}; + } +}); + ipcMain.on(AppChannel.Exit, (event: IpcMainEvent, ...args: any[]) => { appExit(); }); diff --git a/projects/ucap-webmessenger-api/src/lib/models/file-upload-item.ts b/projects/ucap-webmessenger-api/src/lib/models/file-upload-item.ts index 72e09741..786eae35 100644 --- a/projects/ucap-webmessenger-api/src/lib/models/file-upload-item.ts +++ b/projects/ucap-webmessenger-api/src/lib/models/file-upload-item.ts @@ -1,5 +1,6 @@ import { Observable, Subject } from 'rxjs'; import { share } from 'rxjs/operators'; +import { FileUtil } from '@ucap-webmessenger/core'; export class FileUploadItem { file: File; @@ -24,6 +25,24 @@ export class FileUploadItem { return fileItems; } + static fromDataUrls( + fileNameAppend: string, + ...dataUrls: string[] + ): FileUploadItem[] { + const fileItems: FileUploadItem[] = []; + + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < dataUrls.length; i++) { + fileItems.push( + new FileUploadItem( + FileUtil.fromDataUrlToFile(fileNameAppend, dataUrls[i]) + ) + ); + } + + return fileItems; + } + static from(): FileUploadItem { return new FileUploadItem(); } diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.html b/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.html index 95247198..fa08118b 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.html +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.html @@ -327,6 +327,7 @@ (clearView)="clearView()" (toggleStickerSelector)="onShowToggleStickerSelector($event)" (toggleTranslation)="onShowToggleTranslation($event)" + (clipboardPaste)="onClipboardPaste()" > diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts b/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts index 62a655ef..1c92a9b5 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/components/messages.component.ts @@ -8,7 +8,8 @@ import { EventEmitter, Inject, ChangeDetectorRef, - ChangeDetectionStrategy + ChangeDetectionStrategy, + ElementRef } from '@angular/core'; import { ucapAnimations, @@ -130,6 +131,11 @@ import { TranslateService } from '@ngx-translate/core'; import { TranslatePipe } from 'projects/ucap-webmessenger-ui/src/lib/pipes/translate.pipe'; import { TranslateService as UiTranslateService } from '@ucap-webmessenger/ui'; import { FileProtocolService } from '@ucap-webmessenger/protocol-file'; +import { + ClipboardDialogComponent, + ClipboardDialogData, + ClipboardDialogResult +} from '../dialogs/chat/clipboard.dialog.component'; @Component({ selector: 'app-layout-messenger-messages', @@ -1958,4 +1964,47 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit { this.isTranslationProcess = false; this.translationPreviewInfo = null; } + + onClipboardPaste() { + this.nativeService.readFromClipboard().then(async data => { + if (!!data.image && !!data.text) { + const result = await this.dialogService.open< + ClipboardDialogComponent, + ClipboardDialogData, + ClipboardDialogResult + >(ClipboardDialogComponent, { + width: '800px', + maxWidth: '800px', + height: '800px', + minHeight: '800px', + disableClose: false, + data: { + content: data + } + }); + + if (result.selected.text) { + this.onSendMessage(data.text); + } + + if (result.selected.image) { + const fileUploadItems = FileUploadItem.fromDataUrls( + 'clipboard', + data.imageDataUrl + ); + + this.onFileSelected(fileUploadItems); + } + } else if (!!data.image && !data.text) { + const fileUploadItems = FileUploadItem.fromDataUrls( + 'clipboard', + data.imageDataUrl + ); + + this.onFileSelected(fileUploadItems); + } else { + this.chatForm.replyInput.nativeElement.value = `${this.chatForm.replyInput.nativeElement.value}${data.text}`; + } + }); + } } diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.html b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.html new file mode 100644 index 00000000..59713d3e --- /dev/null +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.html @@ -0,0 +1,79 @@ + + + {{ 'settings.label' | translate }} + + + +
+ + + + + {{ + 'common.file.clipboardType.text' | translate + }} + + +
{{ data.content.text }}
+
+
+ + + + + + {{ + 'common.file.clipboardType.image' | translate + }} + + + +
+ +
+
+
+
+
+
+ + + + +
diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.scss b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.scss new file mode 100644 index 00000000..58580a0a --- /dev/null +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.scss @@ -0,0 +1,127 @@ +::ng-deep .setting-frame { + padding: 16px; + height: 100%; + min-width: 500px; + position: relative; + + .mat-dialog-container { + position: relative; + } + + .mat-card-header { + position: relative; + width: 100%; + border-bottom: 1px solid #dddddd; + margin-bottom: 12px; + + .btn-dialog-close { + font-size: 20px; + display: flex; + margin-left: auto; + align-self: flex-start; + color: #444444; + } + } + .mat-card-content { + flex: 0 0 auto; + display: flex; + align-items: flex-start; + height: calc(100% - 100px); + border-bottom: 1px solid #dddddd; + .setting-tab { + position: relative; + width: 100%; + height: 100%; + + .title-text { + padding-left: 5px; + } + + .mat-tab-group { + flex-direction: row; + .mat-tab-header { + width: 160px; + .mat-tab-labels { + flex-direction: column; + .mat-tab-label { + padding: 0 10px; + align-content: flex-start; + text-align: left; + align-items: self-start; + justify-content: flex-start; + } + } + .mat-ink-bar { + display: none; + } + } + } + } + } + + .button-farm { + text-align: right; + position: absolute; + width: 100%; + bottom: 10px; + .mat-primary { + margin-left: 4px; + } + } +} + +::ng-deep .setting-tab { + .mat-tab-group { + position: relative; + height: 100%; + width: 100%; + + .mat-tab-header { + width: 160px; + flex-flow: column; + border-right: 1px solid #dddddd; + .mat-tab-label-container { + .mat-tab-list { + .mat-tab-labels { + border-bottom: 0; + padding-right: 10px; + .mat-tab-label { + padding: 0 10px; + } + } + } + } + } + + .mat-tab-body-wrapper { + .mat-tab-body { + .mat-tab-body-conten { + position: relative; + width: 100%; + height: 100%; + .mat-list-base { + position: relative; + } + } + } + } + } +} + +::ng-deep .setting-category { + .mat-list-base { + position: relative; + .mat-list-item { + font-size: 15px; + .mat-tab-header { + border-right: none; + } + } + .mat-divider { + //margin-top: 10px; + } + .mat-subheader { + font-weight: 600; + } + } +} diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.spec.ts b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.spec.ts new file mode 100644 index 00000000..b7f5b89e --- /dev/null +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClipboardDialogComponent } from './clipboard.dialog.component'; + +describe('ClipboardDialogComponent', () => { + let component: ClipboardDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ClipboardDialogComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClipboardDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.ts b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.ts new file mode 100644 index 00000000..cc55f218 --- /dev/null +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/clipboard.dialog.component.ts @@ -0,0 +1,68 @@ +import { + Component, + OnInit, + Inject, + AfterViewInit, + ViewChild +} from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA, MatCheckbox } from '@angular/material'; + +export interface ClipboardDialogData { + content: { + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }; +} + +export interface ClipboardDialogResult { + selected?: { + text?: boolean; + rtf?: boolean; + html?: boolean; + image?: boolean; + }; +} + +@Component({ + selector: 'app-layout-messenger-clipboard', + templateUrl: './clipboard.dialog.component.html', + styleUrls: ['./clipboard.dialog.component.scss'] +}) +export class ClipboardDialogComponent implements OnInit, AfterViewInit { + @ViewChild('chkText', { static: false }) + chkText: MatCheckbox; + + @ViewChild('chkImage', { static: false }) + chkImage: MatCheckbox; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ClipboardDialogData + ) {} + + ngOnInit() {} + + ngAfterViewInit(): void {} + + onClickChoice(choice: boolean): void { + let selected: { + text?: boolean; + rtf?: boolean; + html?: boolean; + image?: boolean; + }; + + if (choice) { + selected = { + text: !!this.chkText && this.chkText.checked, + image: !!this.chkImage && this.chkImage.checked + }; + } else { + } + + this.dialogRef.close({ selected }); + } +} diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/index.ts b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/index.ts index 5fddc2c8..abfd3972 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/index.ts +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/chat/index.ts @@ -1,9 +1,11 @@ import { CreateChatDialogComponent } from './create-chat.dialog.component'; import { EditChatRoomDialogComponent } from './edit-chat-room.dialog.component'; import { MassDetailComponent } from './mass-detail.component'; +import { ClipboardDialogComponent } from './clipboard.dialog.component'; export const DIALOGS = [ CreateChatDialogComponent, EditChatRoomDialogComponent, - MassDetailComponent + MassDetailComponent, + ClipboardDialogComponent ]; diff --git a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/settings/messenger-settings.dialog.component.scss b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/settings/messenger-settings.dialog.component.scss index be24d055..c4ad3975 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/settings/messenger-settings.dialog.component.scss +++ b/projects/ucap-webmessenger-app/src/app/layouts/messenger/dialogs/settings/messenger-settings.dialog.component.scss @@ -42,7 +42,7 @@ padding: 0 10px; align-content: flex-start; text-align: left; - align-items: self-start; + align-items: center; justify-content: flex-start; } } diff --git a/projects/ucap-webmessenger-app/src/assets/i18n/en.json b/projects/ucap-webmessenger-app/src/assets/i18n/en.json index adf1f035..589ac713 100644 --- a/projects/ucap-webmessenger-app/src/assets/i18n/en.json +++ b/projects/ucap-webmessenger-app/src/assets/i18n/en.json @@ -402,6 +402,13 @@ "openDownloadFolder": "Open download folder", "selectFiles": "Select files", "dropZoneForUpload": "Drop files here to upload.", + "clipboard": "Clipboard", + "clipboardType": { + "text": "Text", + "rtf": "Rich Text Format", + "html": "HTML", + "image": "Image" + }, "type": { "label": "Type of file", "images": "Images", diff --git a/projects/ucap-webmessenger-app/src/assets/i18n/ko.json b/projects/ucap-webmessenger-app/src/assets/i18n/ko.json index 6cafa9e7..73efa1c9 100644 --- a/projects/ucap-webmessenger-app/src/assets/i18n/ko.json +++ b/projects/ucap-webmessenger-app/src/assets/i18n/ko.json @@ -402,6 +402,13 @@ "openDownloadFolder": "다운로드 폴더 열기", "selectFiles": "파일을 선택하세요", "dropZoneForUpload": "여기에 파일을 Drop하시면 업로드 됩니다.", + "clipboard": "클립보드", + "clipboardType": { + "text": "텍스트", + "rtf": "리치 텍스트 포맷", + "html": "HTML", + "image": "이미지" + }, "type": { "label": "파일 종류", "images": "이미지", diff --git a/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts b/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts index eb5e78b5..82290b4d 100644 --- a/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts +++ b/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts @@ -42,6 +42,28 @@ export class FileUtil { }); } + static fromDataUrlToFile(fileNameAppender: string, dataUrl: string): File { + const BASE64_MARKER = ';base64,'; + // tslint:disable-next-line: variable-name + const isDataURI = (_url: string) => _url.split(BASE64_MARKER).length === 2; + + if (!isDataURI(dataUrl)) { + return undefined; + } + + const mime = dataUrl.split(BASE64_MARKER)[0].split(':')[1]; + const filename = + fileNameAppender + new Date().getTime() + '.' + mime.split('/')[1]; + const bytes = atob(dataUrl.split(BASE64_MARKER)[1]); + const writer = new Uint8Array(new ArrayBuffer(bytes.length)); + + for (let i = 0; i < bytes.length; i++) { + writer[i] = bytes.charCodeAt(i); + } + + return new File([writer.buffer], filename, { type: mime }); + } + static fromBlobToString(blob: Blob): Promise { return new Promise((resolve, reject) => { const fileReader = new FileReader(); diff --git a/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts b/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts index 50bec0b9..0bf8371d 100644 --- a/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts +++ b/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts @@ -281,6 +281,24 @@ export class BrowserNativeService extends NativeService { open(url); } + readFromClipboard(): Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }> { + return new Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }>((resolve, reject) => { + resolve({}); + }); + } + constructor(private httpClient: HttpClient) { super(); this.notificationService = new NotificationService(); diff --git a/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts b/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts index 7f545324..90aa84fc 100644 --- a/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts +++ b/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts @@ -23,7 +23,8 @@ import { MessengerChannel, MessageChannel, ProcessChannel, - AppChannel + AppChannel, + ClipboardChannel } from '../types/channel.type'; import { Injectable } from '@angular/core'; import { TranslateLoaderService } from '../translate/electron-loader'; @@ -470,6 +471,28 @@ export class ElectronNativeService implements NativeService { this.shell.openExternal(url); } + readFromClipboard(): Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }> { + return new Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }>((resolve, reject) => { + try { + resolve(this.ipcRenderer.sendSync(ClipboardChannel.Read)); + } catch (error) { + reject(error); + } + }); + } + get isElectron() { return window && (window as any).process && (window as any).process.type; } diff --git a/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts b/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts index fdc1801f..1348c92a 100644 --- a/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts +++ b/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts @@ -55,6 +55,10 @@ export enum IdleStateChannel { ChangeLimitTime = 'UCAP::idleState::changeLimitTime' } +export enum ClipboardChannel { + Read = 'UCAP::clipboard::read' +} + export enum AppChannel { Exit = 'UCAP::app::exit' } diff --git a/projects/ucap-webmessenger-native/src/lib/services/native.service.ts b/projects/ucap-webmessenger-native/src/lib/services/native.service.ts index ec7962a8..80ec8fde 100644 --- a/projects/ucap-webmessenger-native/src/lib/services/native.service.ts +++ b/projects/ucap-webmessenger-native/src/lib/services/native.service.ts @@ -88,4 +88,12 @@ export abstract class NativeService { ): TranslateLoader; abstract openDefaultBrowser(url: string): void; + + abstract readFromClipboard(): Promise<{ + text?: string; + rtf?: string; + html?: string; + image?: Buffer; + imageDataUrl?: string; + }>; } diff --git a/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.html b/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.html index ba79f97b..4cb45715 100644 --- a/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.html +++ b/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.html @@ -108,6 +108,7 @@ name="message" [matTextareaAutosize]="true" [matAutosizeMaxRows]="20" + (paste)="onPasteReply($event)" > diff --git a/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.ts b/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.ts index b1cd3b0a..e3fd2f79 100644 --- a/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.ts +++ b/projects/ucap-webmessenger-ui-chat/src/lib/components/form.component.ts @@ -34,6 +34,9 @@ export class FormComponent implements OnInit { @Output() clearView = new EventEmitter(); + @Output() + clipboardPaste = new EventEmitter(); + @ViewChild('replyForm', { static: false }) replyForm: NgForm; @@ -84,4 +87,10 @@ export class FormComponent implements OnInit { onClickTranslation() { this.toggleTranslation.emit(); } + + onPasteReply(event: ClipboardEvent) { + event.preventDefault(); + + this.clipboardPaste.emit(); + } } diff --git a/projects/ucap-webmessenger-ui/src/lib/pipes/safe-html.pipe.ts b/projects/ucap-webmessenger-ui/src/lib/pipes/safe-html.pipe.ts new file mode 100644 index 00000000..40237093 --- /dev/null +++ b/projects/ucap-webmessenger-ui/src/lib/pipes/safe-html.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Pipe({ name: 'ucapSafeHtml' }) +export class SafeHtmlPipe implements PipeTransform { + constructor(private domSanitizer: DomSanitizer) {} + + public transform(value: string) { + return this.domSanitizer.bypassSecurityTrustHtml(value); + } +} diff --git a/projects/ucap-webmessenger-ui/src/lib/ucap-ui.module.ts b/projects/ucap-webmessenger-ui/src/lib/ucap-ui.module.ts index c8d2de5a..ce0d2ea4 100644 --- a/projects/ucap-webmessenger-ui/src/lib/ucap-ui.module.ts +++ b/projects/ucap-webmessenger-ui/src/lib/ucap-ui.module.ts @@ -71,6 +71,7 @@ import { SecondsToMinutesPipe } from './pipes/seconds-to-minutes.pipe'; import { LinkyPipe } from './pipes/linky.pipe'; import { TranslatePipe } from './pipes/translate.pipe'; import { DatePipe } from './pipes/date.pipe'; +import { SafeHtmlPipe } from './pipes/safe-html.pipe'; import { StringEmptyCheckPipe, @@ -125,7 +126,8 @@ const PIPES = [ TranslatePipe, DatePipe, StringEmptyCheckPipe, - StringFormatterPhonePipe + StringFormatterPhonePipe, + SafeHtmlPipe ]; const SERVICES = [ BottomSheetService,