add sharex config exporter
This commit is contained in:
parent
9580ccc928
commit
94763e1e41
|
@ -31,6 +31,7 @@
|
|||
"fuse.js": "^6.6.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"moment": "^2.29.4",
|
||||
"ng-mat-select-infinite-scroll": "^4.0.0",
|
||||
"ngx-auto-unsubscribe-decorator": "^1.1.0",
|
||||
"ngx-dropzone": "^3.1.0",
|
||||
"ngx-moment": "^6.0.2",
|
||||
|
|
|
@ -1,2 +1,43 @@
|
|||
<h1>Create a ShareX target config</h1>
|
||||
<div *ngIf="available > 0">
|
||||
<h1>Create a ShareX target config</h1>
|
||||
|
||||
<p>Please select an api key to associate with the ShareX target.</p>
|
||||
|
||||
<div class="row">
|
||||
<mat-form-field class="col-12 col-md-6 col-xl-4" appearance="outline">
|
||||
<mat-label>Api Key</mat-label>
|
||||
<mat-select
|
||||
msInfiniteScroll
|
||||
(selectionChange)="onSelectionChange($event)"
|
||||
(infiniteScroll)="getNextBatch()"
|
||||
[complete]="loaded === available"
|
||||
>
|
||||
<mat-option *ngFor="let key of apikeys$ | async" [value]="key.key">{{
|
||||
key.name
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button mat-raised-button color="accent" [disabled]="key === null" (click)="onExport()">
|
||||
Export Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="available === 0">
|
||||
<h1>No API keys available</h1>
|
||||
<p>You need to have at least one API key to create a ShareX target config.</p>
|
||||
|
||||
<button mat-raised-button color="accent" routerLink="/settings/apikeys">
|
||||
Create an API key here
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="available < 0">
|
||||
<h1>Loading</h1>
|
||||
<mat-spinner color="accent"></mat-spinner>
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,88 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
|
||||
import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { scan } from 'rxjs/operators';
|
||||
import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
|
||||
import { ApiKeysService } from 'src/app/services/api/apikeys.service';
|
||||
import { PermissionService } from 'src/app/services/api/permission.service';
|
||||
import { Logger } from 'src/app/services/logger/logger.service';
|
||||
import { SimpleUtilService } from 'src/app/util/util-module/simple-util.service';
|
||||
import { UtilService } from 'src/app/util/util-module/util.service';
|
||||
import { BuildShareX } from './sharex-builder';
|
||||
|
||||
@Component({
|
||||
templateUrl: './settings-sharex.component.html',
|
||||
styleUrls: ['./settings-sharex.component.scss'],
|
||||
})
|
||||
export class SettingsShareXComponent {
|
||||
export class SettingsShareXComponent implements OnInit {
|
||||
private readonly logger = new Logger(SettingsShareXComponent.name);
|
||||
|
||||
public apikeys: EApiKey[] = [];
|
||||
public apikeys = new BehaviorSubject<EApiKey[]>([]);
|
||||
public apikeys$ = this.apikeys.asObservable().pipe(
|
||||
scan((acc, curr) => {
|
||||
return [...acc, ...curr];
|
||||
}, [] as EApiKey[]),
|
||||
);
|
||||
|
||||
constructor(private readonly apikeysService: ApiKeysService) {}
|
||||
public loaded = 0;
|
||||
public available = -1;
|
||||
|
||||
private async loadApiKeys() {
|
||||
public key: string | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly apikeysService: ApiKeysService,
|
||||
private readonly utilService: UtilService,
|
||||
private readonly simpleUtil: SimpleUtilService,
|
||||
private readonly permissionService: PermissionService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getNextBatch();
|
||||
}
|
||||
|
||||
onSelectionChange(event: MatSelectChange) {
|
||||
this.key = event.value;
|
||||
}
|
||||
|
||||
async onExport() {
|
||||
if (this.key === null) return;
|
||||
|
||||
const permissions = await this.permissionService.getLoadedSnapshot();
|
||||
const canUseDelete = permissions.includes(Permission.ImageDeleteKey);
|
||||
|
||||
const sharexConfig = BuildShareX(
|
||||
this.simpleUtil.getHost(),
|
||||
this.key,
|
||||
canUseDelete,
|
||||
);
|
||||
|
||||
this.simpleUtil.downloadBuffer(
|
||||
JSON.stringify(sharexConfig),
|
||||
'Pisur-ShareX-target.sxcu',
|
||||
'application/json',
|
||||
);
|
||||
|
||||
this.utilService.showSnackBar(
|
||||
'Exported ShareX config',
|
||||
SnackBarType.Success,
|
||||
);
|
||||
}
|
||||
|
||||
async getNextBatch() {
|
||||
const newApiKeys = await this.apikeysService.getApiKeys(
|
||||
50,
|
||||
Math.floor(this.loaded / 50),
|
||||
);
|
||||
if (HasFailed(newApiKeys)) {
|
||||
this.utilService.showSnackBar(newApiKeys.getReason(), SnackBarType.Error);
|
||||
return;
|
||||
}
|
||||
this.loaded += newApiKeys.results.length;
|
||||
this.available = newApiKeys.total;
|
||||
|
||||
this.apikeys.next(newApiKeys.results);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSelectInfiniteScrollModule } from 'ng-mat-select-infinite-scroll';
|
||||
import { UtilModule } from 'src/app/util/util-module/util.module';
|
||||
import { SettingsShareXComponent } from './settings-sharex.component';
|
||||
import { SettingsShareXRoutingModule } from './settings-sharex.routing.module';
|
||||
|
||||
|
@ -8,6 +14,12 @@ import { SettingsShareXRoutingModule } from './settings-sharex.routing.module';
|
|||
imports: [
|
||||
CommonModule,
|
||||
SettingsShareXRoutingModule,
|
||||
MatSelectModule,
|
||||
MatSelectInfiniteScrollModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
UtilModule,
|
||||
],
|
||||
})
|
||||
export class SettingsShareXRouteModule {}
|
||||
|
|
64
frontend/src/app/routes/settings/sharex/sharex-builder.ts
Normal file
64
frontend/src/app/routes/settings/sharex/sharex-builder.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
{
|
||||
"Version": "14.1.0",
|
||||
"Name": "Picsur",
|
||||
"DestinationType": "ImageUploader",
|
||||
"RequestMethod": "POST",
|
||||
"RequestURL": "https://d290-87-208-4-195.eu.ngrok.io/api/image/upload",
|
||||
"Headers": {
|
||||
"Authorization": "Api-Key Hello"
|
||||
},
|
||||
"Body": "MultipartFormData",
|
||||
"FileFormName": "image",
|
||||
"URL": "https://d290-87-208-4-195.eu.ngrok.io/view/{json:data.id}",
|
||||
"ThumbnailURL": "https://d290-87-208-4-195.eu.ngrok.io/i/{json:data.id}.png?width=256&shrinkonly=yes",
|
||||
"DeletionURL": "https://d290-87-208-4-195.eu.ngrok.io/api/image/delete/{json:data.id}/{json:data.delete_key}",
|
||||
"ErrorMessage": "{json:data.message}"
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export interface ShareXObject {
|
||||
Version: string;
|
||||
Name: string;
|
||||
DestinationType: string;
|
||||
RequestMethod: string;
|
||||
RequestURL: string;
|
||||
Headers: {
|
||||
Authorization: string;
|
||||
};
|
||||
Body: string;
|
||||
FileFormName: string;
|
||||
URL: string;
|
||||
ThumbnailURL: string;
|
||||
DeletionURL?: string;
|
||||
ErrorMessage: string;
|
||||
}
|
||||
|
||||
export function BuildShareX(
|
||||
host: string,
|
||||
apikey: string,
|
||||
canDelete: boolean,
|
||||
): ShareXObject {
|
||||
const base: ShareXObject = {
|
||||
Version: '14.1.0',
|
||||
Name: 'Picsur',
|
||||
DestinationType: 'ImageUploader',
|
||||
RequestMethod: 'POST',
|
||||
RequestURL: `${host}/api/image/upload`,
|
||||
Headers: {
|
||||
Authorization: `Api-Key ${apikey}`,
|
||||
},
|
||||
Body: 'MultipartFormData',
|
||||
FileFormName: 'image',
|
||||
URL: `${host}/view/{json:data.id}`,
|
||||
ThumbnailURL: `${host}/i/{json:data.id}.png?width=256&shrinkonly=yes`,
|
||||
ErrorMessage: '{json:data.message}',
|
||||
};
|
||||
|
||||
if (canDelete) {
|
||||
base.DeletionURL = `${host}/api/image/delete/{json:data.id}/{json:data.delete_key}`;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { Inject, Injectable } from '@angular/core';
|
||||
import { LOCATION } from '@ng-web-apis/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ImageDeleteRequest,
|
||||
ImageDeleteResponse,
|
||||
|
@ -22,6 +21,7 @@ import {
|
|||
HasSuccess,
|
||||
Open
|
||||
} from 'picsur-shared/dist/types/failable';
|
||||
import { SimpleUtilService } from 'src/app/util/util-module/simple-util.service';
|
||||
import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
|
||||
import { ApiService } from './api.service';
|
||||
import { UserService } from './user.service';
|
||||
|
@ -32,7 +32,7 @@ import { UserService } from './user.service';
|
|||
export class ImageService {
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
@Inject(LOCATION) private readonly location: Location,
|
||||
private readonly simpleUtil: SimpleUtilService,
|
||||
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
@ -110,7 +110,7 @@ export class ImageService {
|
|||
// Non api calls
|
||||
|
||||
public GetImageURL(image: string, filetype: string | null): string {
|
||||
const baseURL = this.location.protocol + '//' + this.location.host;
|
||||
const baseURL = this.simpleUtil.getHost();
|
||||
const extension = FileType2Ext(filetype ?? '');
|
||||
|
||||
return `${baseURL}/i/${image}${
|
||||
|
|
30
frontend/src/app/util/util-module/simple-util.service.ts
Normal file
30
frontend/src/app/util/util-module/simple-util.service.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Inject, Injectable } from '@angular/core';
|
||||
import { LOCATION } from '@ng-web-apis/common';
|
||||
import { Logger } from '../../services/logger/logger.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'any',
|
||||
})
|
||||
export class SimpleUtilService {
|
||||
private readonly logger = new Logger(SimpleUtilService.name);
|
||||
|
||||
constructor(@Inject(LOCATION) private readonly location: Location) {}
|
||||
|
||||
public getHost(): string {
|
||||
return this.location.protocol + '//' + this.location.host;
|
||||
}
|
||||
|
||||
public downloadBuffer(
|
||||
buffer: ArrayBuffer | string,
|
||||
filename: string,
|
||||
filetype: string = 'application/octet-stream',
|
||||
) {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(
|
||||
new Blob([buffer], { type: filetype }),
|
||||
);
|
||||
a.download = filename;
|
||||
a.target = '_self';
|
||||
a.click();
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||
ConfirmDialogData
|
||||
} from './confirm-dialog/confirm-dialog.component';
|
||||
import { DownloadDialogComponent } from './download-dialog/download-dialog.component';
|
||||
import { SimpleUtilService } from './simple-util.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'any',
|
||||
|
@ -21,6 +22,7 @@ export class UtilService {
|
|||
private readonly logger = new Logger(UtilService.name);
|
||||
|
||||
constructor(
|
||||
private readonly simpleUtil: SimpleUtilService,
|
||||
private readonly snackBar: MatSnackBar,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly router: Router,
|
||||
|
@ -98,14 +100,7 @@ export class UtilService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Download with the browser
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(
|
||||
new Blob([file.buffer], { type: file.mimeType }),
|
||||
);
|
||||
a.download = file.name;
|
||||
a.target = '_self';
|
||||
a.click();
|
||||
this.simpleUtil.downloadBuffer(file.buffer, file.name, file.mimeType);
|
||||
|
||||
closeDialog();
|
||||
this.showSnackBar('Image downloaded', SnackBarType.Info);
|
||||
|
|
|
@ -20,6 +20,13 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.center-horizontally {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -8067,6 +8067,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ng-mat-select-infinite-scroll@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "ng-mat-select-infinite-scroll@npm:4.0.0"
|
||||
dependencies:
|
||||
tslib: ^2.3.0
|
||||
peerDependencies:
|
||||
"@angular/core": ">=6.0.0 <15.0.0"
|
||||
"@angular/material": ">=6.0.0 <15.0.0"
|
||||
checksum: 94f61ace2b06c8c6951704b55535e66e251e68e4d37fbd177ff13d839b874536b94181a54b633783bde2938b730954c44cb61070cae08859d91f70904fd898eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ngx-auto-unsubscribe-decorator@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "ngx-auto-unsubscribe-decorator@npm:1.1.0"
|
||||
|
@ -9058,6 +9070,7 @@ __metadata:
|
|||
fuse.js: ^6.6.2
|
||||
jwt-decode: ^3.1.2
|
||||
moment: ^2.29.4
|
||||
ng-mat-select-infinite-scroll: ^4.0.0
|
||||
ngx-auto-unsubscribe-decorator: ^1.1.0
|
||||
ngx-dropzone: ^3.1.0
|
||||
ngx-moment: ^6.0.2
|
||||
|
|
Loading…
Reference in a new issue