clipboard

This commit is contained in:
richard-loafle 2020-02-04 17:13:09 +09:00
parent ad78e23303
commit ef007d0ab5
21 changed files with 515 additions and 9 deletions

View File

@ -23,8 +23,8 @@ export class AppWindow {
// tslint:disable-next-line: variable-name // tslint:disable-next-line: variable-name
private _rendererReadyTime: number | null = null; private _rendererReadyTime: number | null = null;
private minWidth = 700; private minWidth = 1160;
private minHeight = 600; private minHeight = 800;
public constructor(private appIconPath: string) { public constructor(private appIconPath: string) {
const savedWindowState = windowStateKeeper({ const savedWindowState = windowStateKeeper({
@ -165,7 +165,11 @@ export class AppWindow {
} else { } else {
this.window.loadURL( this.window.loadURL(
url.format({ url.format({
pathname: path.join(__dirname, '..', 'ucap-webmessenger-app/index.html'), pathname: path.join(
__dirname,
'..',
'ucap-webmessenger-app/index.html'
),
protocol: 'file:', protocol: 'file:',
slashes: true slashes: true
}) })

View File

@ -6,7 +6,8 @@ import {
Menu, Menu,
shell, shell,
dialog, dialog,
BrowserWindow BrowserWindow,
clipboard
} from 'electron'; } from 'electron';
import path from 'path'; import path from 'path';
import fse from 'fs-extra'; import fse from 'fs-extra';
@ -29,7 +30,8 @@ import {
ChatChannel, ChatChannel,
MessengerChannel, MessengerChannel,
MessageChannel, MessageChannel,
AppChannel AppChannel,
ClipboardChannel
} from '@ucap-webmessenger/native-electron'; } from '@ucap-webmessenger/native-electron';
import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification'; import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification';
import { ElectronUpdateWindowService } from '@ucap-webmessenger/electron-update-window'; 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[]) => { ipcMain.on(AppChannel.Exit, (event: IpcMainEvent, ...args: any[]) => {
appExit(); appExit();
}); });

View File

@ -1,5 +1,6 @@
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { share } from 'rxjs/operators'; import { share } from 'rxjs/operators';
import { FileUtil } from '@ucap-webmessenger/core';
export class FileUploadItem { export class FileUploadItem {
file: File; file: File;
@ -24,6 +25,24 @@ export class FileUploadItem {
return fileItems; 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 { static from(): FileUploadItem {
return new FileUploadItem(); return new FileUploadItem();
} }

View File

@ -327,6 +327,7 @@
(clearView)="clearView()" (clearView)="clearView()"
(toggleStickerSelector)="onShowToggleStickerSelector($event)" (toggleStickerSelector)="onShowToggleStickerSelector($event)"
(toggleTranslation)="onShowToggleTranslation($event)" (toggleTranslation)="onShowToggleTranslation($event)"
(clipboardPaste)="onClipboardPaste()"
></ucap-chat-form> ></ucap-chat-form>
<!-- / REPLY FORM --> <!-- / REPLY FORM -->
</div> </div>

View File

@ -8,7 +8,8 @@ import {
EventEmitter, EventEmitter,
Inject, Inject,
ChangeDetectorRef, ChangeDetectorRef,
ChangeDetectionStrategy ChangeDetectionStrategy,
ElementRef
} from '@angular/core'; } from '@angular/core';
import { import {
ucapAnimations, ucapAnimations,
@ -130,6 +131,11 @@ import { TranslateService } from '@ngx-translate/core';
import { TranslatePipe } from 'projects/ucap-webmessenger-ui/src/lib/pipes/translate.pipe'; import { TranslatePipe } from 'projects/ucap-webmessenger-ui/src/lib/pipes/translate.pipe';
import { TranslateService as UiTranslateService } from '@ucap-webmessenger/ui'; import { TranslateService as UiTranslateService } from '@ucap-webmessenger/ui';
import { FileProtocolService } from '@ucap-webmessenger/protocol-file'; import { FileProtocolService } from '@ucap-webmessenger/protocol-file';
import {
ClipboardDialogComponent,
ClipboardDialogData,
ClipboardDialogResult
} from '../dialogs/chat/clipboard.dialog.component';
@Component({ @Component({
selector: 'app-layout-messenger-messages', selector: 'app-layout-messenger-messages',
@ -1960,4 +1966,47 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.isTranslationProcess = false; this.isTranslationProcess = false;
this.translationPreviewInfo = null; 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}`;
}
});
}
} }

View File

@ -0,0 +1,79 @@
<mat-card class="confirm-card mat-elevation-z setting-frame">
<mat-card-header cdkDrag cdkDragRootElement=".cdk-overlay-pane" cdkDragHandle>
<mat-card-title>{{ 'settings.label' | translate }}</mat-card-title>
<button class="icon-button btn-dialog-close" (click)="onClickChoice(false)">
<i class="mdi mdi-window-close"></i>
</button>
</mat-card-header>
<mat-card-content>
<div fxFlex class="setting-tab">
<mat-tab-group animationDuration="0ms">
<mat-tab *ngIf="data.content.text">
<ng-template mat-tab-label>
<mat-checkbox #chkText> </mat-checkbox>
<span class="title-text">{{
'common.file.clipboardType.text' | translate
}}</span>
</ng-template>
<perfect-scrollbar>
<div>{{ data.content.text }}</div>
</perfect-scrollbar>
</mat-tab>
<!-- <mat-tab *ngIf="data.content.rtf">
<ng-template mat-tab-label>
<mat-checkbox #chkRtf="matCheckbox"> </mat-checkbox>
<span class="title-text">{{
'common.file.clipboardType.rtf' | translate
}}</span>
</ng-template>
<perfect-scrollbar>
<div>{{ data.content.rtf }}</div>
</perfect-scrollbar>
</mat-tab> -->
<!-- <mat-tab *ngIf="data.content.html">
<ng-template mat-tab-label>
<mat-checkbox #chkHtml="matCheckbox"> </mat-checkbox>
<span class="title-text">{{
'common.file.clipboardType.html' | translate
}}</span>
</ng-template>
<perfect-scrollbar>
<div fxFlexFill>
<table
fxFlexFill
[innerHTML]="data.content.html | ucapSafeHtml"
></table>
</div>
</perfect-scrollbar>
</mat-tab> -->
<mat-tab *ngIf="data.content.image && data.content.imageDataUrl">
<ng-template mat-tab-label>
<mat-checkbox #chkImage> </mat-checkbox>
<span class="title-text">{{
'common.file.clipboardType.image' | translate
}}</span>
</ng-template>
<perfect-scrollbar>
<div>
<img [src]="data.content.imageDataUrl" />
</div>
</perfect-scrollbar>
</mat-tab>
</mat-tab-group>
</div>
</mat-card-content>
<mat-card-actions class="button-farm flex-row">
<button
mat-stroked-button
(click)="onClickChoice(false)"
class="mat-primary"
>
{{ 'common.messages.no' | translate }}
</button>
<button mat-flat-button (click)="onClickChoice(true)" class="mat-primary">
{{ 'common.messages.yes' | translate }}
</button>
</mat-card-actions>
</mat-card>

View File

@ -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;
}
}
}

View File

@ -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<ClipboardDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ClipboardDialogComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClipboardDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<ClipboardDialogData, ClipboardDialogResult>,
@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 });
}
}

View File

@ -1,9 +1,11 @@
import { CreateChatDialogComponent } from './create-chat.dialog.component'; import { CreateChatDialogComponent } from './create-chat.dialog.component';
import { EditChatRoomDialogComponent } from './edit-chat-room.dialog.component'; import { EditChatRoomDialogComponent } from './edit-chat-room.dialog.component';
import { MassDetailComponent } from './mass-detail.component'; import { MassDetailComponent } from './mass-detail.component';
import { ClipboardDialogComponent } from './clipboard.dialog.component';
export const DIALOGS = [ export const DIALOGS = [
CreateChatDialogComponent, CreateChatDialogComponent,
EditChatRoomDialogComponent, EditChatRoomDialogComponent,
MassDetailComponent MassDetailComponent,
ClipboardDialogComponent
]; ];

View File

@ -402,6 +402,13 @@
"openDownloadFolder": "Open download folder", "openDownloadFolder": "Open download folder",
"selectFiles": "Select files", "selectFiles": "Select files",
"dropZoneForUpload": "Drop files here to upload.", "dropZoneForUpload": "Drop files here to upload.",
"clipboard": "Clipboard",
"clipboardType": {
"text": "Text",
"rtf": "Rich Text Format",
"html": "HTML",
"image": "Image"
},
"type": { "type": {
"label": "Type of file", "label": "Type of file",
"images": "Images", "images": "Images",

View File

@ -402,6 +402,13 @@
"openDownloadFolder": "다운로드 폴더 열기", "openDownloadFolder": "다운로드 폴더 열기",
"selectFiles": "파일을 선택하세요", "selectFiles": "파일을 선택하세요",
"dropZoneForUpload": "여기에 파일을 Drop하시면 업로드 됩니다.", "dropZoneForUpload": "여기에 파일을 Drop하시면 업로드 됩니다.",
"clipboard": "클립보드",
"clipboardType": {
"text": "텍스트",
"rtf": "리치 텍스트 포맷",
"html": "HTML",
"image": "이미지"
},
"type": { "type": {
"label": "파일 종류", "label": "파일 종류",
"images": "이미지", "images": "이미지",

View File

@ -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<string> { static fromBlobToString(blob: Blob): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
const fileReader = new FileReader(); const fileReader = new FileReader();

View File

@ -281,6 +281,24 @@ export class BrowserNativeService extends NativeService {
open(url); 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) { constructor(private httpClient: HttpClient) {
super(); super();
this.notificationService = new NotificationService(); this.notificationService = new NotificationService();

View File

@ -23,7 +23,8 @@ import {
MessengerChannel, MessengerChannel,
MessageChannel, MessageChannel,
ProcessChannel, ProcessChannel,
AppChannel AppChannel,
ClipboardChannel
} from '../types/channel.type'; } from '../types/channel.type';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateLoaderService } from '../translate/electron-loader'; import { TranslateLoaderService } from '../translate/electron-loader';
@ -470,6 +471,28 @@ export class ElectronNativeService implements NativeService {
this.shell.openExternal(url); 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() { get isElectron() {
return window && (window as any).process && (window as any).process.type; return window && (window as any).process && (window as any).process.type;
} }

View File

@ -55,6 +55,10 @@ export enum IdleStateChannel {
ChangeLimitTime = 'UCAP::idleState::changeLimitTime' ChangeLimitTime = 'UCAP::idleState::changeLimitTime'
} }
export enum ClipboardChannel {
Read = 'UCAP::clipboard::read'
}
export enum AppChannel { export enum AppChannel {
Exit = 'UCAP::app::exit' Exit = 'UCAP::app::exit'
} }

View File

@ -88,4 +88,12 @@ export abstract class NativeService {
): TranslateLoader; ): TranslateLoader;
abstract openDefaultBrowser(url: string): void; abstract openDefaultBrowser(url: string): void;
abstract readFromClipboard(): Promise<{
text?: string;
rtf?: string;
html?: string;
image?: Buffer;
imageDataUrl?: string;
}>;
} }

View File

@ -108,6 +108,7 @@
name="message" name="message"
[matTextareaAutosize]="true" [matTextareaAutosize]="true"
[matAutosizeMaxRows]="20" [matAutosizeMaxRows]="20"
(paste)="onPasteReply($event)"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>

View File

@ -34,6 +34,9 @@ export class FormComponent implements OnInit {
@Output() @Output()
clearView = new EventEmitter(); clearView = new EventEmitter();
@Output()
clipboardPaste = new EventEmitter();
@ViewChild('replyForm', { static: false }) @ViewChild('replyForm', { static: false })
replyForm: NgForm; replyForm: NgForm;
@ -84,4 +87,10 @@ export class FormComponent implements OnInit {
onClickTranslation() { onClickTranslation() {
this.toggleTranslation.emit(); this.toggleTranslation.emit();
} }
onPasteReply(event: ClipboardEvent) {
event.preventDefault();
this.clipboardPaste.emit();
}
} }

View File

@ -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);
}
}

View File

@ -71,6 +71,7 @@ import { SecondsToMinutesPipe } from './pipes/seconds-to-minutes.pipe';
import { LinkyPipe } from './pipes/linky.pipe'; import { LinkyPipe } from './pipes/linky.pipe';
import { TranslatePipe } from './pipes/translate.pipe'; import { TranslatePipe } from './pipes/translate.pipe';
import { DatePipe } from './pipes/date.pipe'; import { DatePipe } from './pipes/date.pipe';
import { SafeHtmlPipe } from './pipes/safe-html.pipe';
import { import {
StringEmptyCheckPipe, StringEmptyCheckPipe,
@ -125,7 +126,8 @@ const PIPES = [
TranslatePipe, TranslatePipe,
DatePipe, DatePipe,
StringEmptyCheckPipe, StringEmptyCheckPipe,
StringFormatterPhonePipe StringFormatterPhonePipe,
SafeHtmlPipe
]; ];
const SERVICES = [ const SERVICES = [
BottomSheetService, BottomSheetService,