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

View File

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

View File

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

View File

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

View File

@ -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',
@ -1960,4 +1966,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}`;
}
});
}
}

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 { 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
];

View File

@ -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",

View File

@ -402,6 +402,13 @@
"openDownloadFolder": "다운로드 폴더 열기",
"selectFiles": "파일을 선택하세요",
"dropZoneForUpload": "여기에 파일을 Drop하시면 업로드 됩니다.",
"clipboard": "클립보드",
"clipboardType": {
"text": "텍스트",
"rtf": "리치 텍스트 포맷",
"html": "HTML",
"image": "이미지"
},
"type": {
"label": "파일 종류",
"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> {
return new Promise<string>((resolve, reject) => {
const fileReader = new FileReader();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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,