add sharex config exporter

This commit is contained in:
rubikscraft 2022-09-04 12:33:37 +02:00
parent 9580ccc928
commit 94763e1e41
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
10 changed files with 250 additions and 18 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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}${

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

View file

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

View file

@ -20,6 +20,13 @@
width: 100%;
}
.center-horizontally {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.text-center {
text-align: center;
}

View file

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