Change settings layout
This commit is contained in:
parent
3b50cdeb1e
commit
145ff6973f
|
@ -58,7 +58,7 @@ export class ImageConverterService {
|
|||
return Fail(FT.Internal, 'Failed to get conversion limits');
|
||||
}
|
||||
let timeLimitMS = ms(timeLimit as any);
|
||||
if (isNaN(timeLimitMS)) timeLimitMS = 15 * 1000; // 15 seconds
|
||||
if (isNaN(timeLimitMS) || timeLimitMS === 0) timeLimitMS = 15 * 1000; // 15 seconds
|
||||
|
||||
const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit);
|
||||
const sharpOptions: SharpOptions = {
|
||||
|
|
|
@ -1,21 +1,8 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Logger,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Logger, Post, Req, Res } from '@nestjs/common';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { Fail, FT, ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { ReturnsAnything } from '../../../decorators/returns.decorator';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import type AuthFastifyRequest from '../../../models/interfaces/authrequest.dto';
|
||||
import { SysPrefController } from '../pref/sys-pref.controller';
|
||||
import { SysPreferenceDbService } from '../../../collections/preference-db/sys-preference-db.service';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { Fail, FT, ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { URLRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
import { UsageService } from '../../../managers/usage/usage.service';
|
||||
|
||||
@Controller('api/usage')
|
||||
|
@ -28,6 +15,7 @@ export class UsageController {
|
|||
@Post(['report', 'report/*'])
|
||||
@ReturnsAnything()
|
||||
async deleteRole(
|
||||
@Req() req: FastifyRequest,
|
||||
@Res({
|
||||
passthrough: true,
|
||||
})
|
||||
|
@ -40,11 +28,18 @@ export class UsageController {
|
|||
}
|
||||
|
||||
await res.from(`${trackingUrl}/api`, {
|
||||
rewriteRequestHeaders(req, headers) {
|
||||
rewriteRequestHeaders(request, headers) {
|
||||
const req = request as any as FastifyRequest;
|
||||
|
||||
// remove cookies
|
||||
delete headers.cookie;
|
||||
|
||||
// Add real ip, this should not work, but ackee uses a bad ip resolver
|
||||
// So we might aswell use it
|
||||
headers['X-Forwarded-For'] = req.ip;
|
||||
|
||||
return headers;
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +1,96 @@
|
|||
<ng-container *ngIf="pref.type === 'string'">
|
||||
<div class="row">
|
||||
<div class="y-center col-md-6 col-12">
|
||||
<h3 mat-line>{{ name }}</h3>
|
||||
</div>
|
||||
<div class="y-center col-md-6 col-12">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
color="primary"
|
||||
subscriptSizing="dynamic"
|
||||
<div class="y-center">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ name }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="pref.value"
|
||||
(change)="stringUpdateWrapper($event)"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
placeholder="Empty"
|
||||
/>
|
||||
<!-- show tooltip on press -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
(click)="tooltip.show()"
|
||||
*ngIf="helpText !== ''"
|
||||
>
|
||||
<input
|
||||
matInput
|
||||
[value]="pref.value"
|
||||
(change)="stringUpdateWrapper($event)"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-icon
|
||||
matSuffix
|
||||
fontSet="material-icons-outlined"
|
||||
#tooltip="matTooltip"
|
||||
[matTooltip]="helpText"
|
||||
matTooltipPosition="left"
|
||||
matTooltipHideDelay="0"
|
||||
>
|
||||
help_outline
|
||||
</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="pref.type === 'number'">
|
||||
<div class="row">
|
||||
<div class="y-center col-md-6 col-12">
|
||||
<h3 mat-line>{{ name }}</h3>
|
||||
</div>
|
||||
<div class="y-center col-md-6 col-12">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
color="primary"
|
||||
subscriptSizing="dynamic"
|
||||
<div class="y-center">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ name }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[value]="pref.value"
|
||||
(change)="numberUpdateWrapper($event)"
|
||||
placeholder="Empty"
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
(click)="tooltip.show()"
|
||||
*ngIf="helpText !== ''"
|
||||
>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[value]="pref.value"
|
||||
(change)="numberUpdateWrapper($event)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-icon
|
||||
matSuffix
|
||||
fontSet="material-icons-outlined"
|
||||
#tooltip="matTooltip"
|
||||
[matTooltip]="helpText"
|
||||
matTooltipPosition="left"
|
||||
matTooltipHideDelay="0"
|
||||
>
|
||||
help_outline
|
||||
</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="pref.type === 'boolean'">
|
||||
<div class="row">
|
||||
<div class="y-center col-md-6 col-12">
|
||||
<h3 mat-line>{{ name }}</h3>
|
||||
</div>
|
||||
<div class="y-center col-md-6 col-12">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
class="col-md-6 col-12"
|
||||
[checked]="valBool"
|
||||
(change)="update($event.checked)"
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
<div class="y-center">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ name }}</mat-label>
|
||||
<mat-select
|
||||
[value]="pref.value"
|
||||
(valueChange)="booleanUpdateWrapper($event)"
|
||||
>
|
||||
<mat-option [value]="false">No</mat-option>
|
||||
<mat-option [value]="true">Yes</mat-option>
|
||||
</mat-select>
|
||||
<button
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
(click)="tooltip.show()"
|
||||
*ngIf="helpText !== ''"
|
||||
>
|
||||
<mat-icon
|
||||
fontSet="material-icons-outlined"
|
||||
#tooltip="matTooltip"
|
||||
[matTooltip]="helpText"
|
||||
matTooltipPosition="left"
|
||||
matTooltipHideDelay="0"
|
||||
>
|
||||
help_outline
|
||||
</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
|||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import {
|
||||
DecodedPref,
|
||||
PrefValueType,
|
||||
PrefValueType
|
||||
} from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@ -24,9 +24,9 @@ export class PrefOptionComponent implements OnInit {
|
|||
key: string,
|
||||
pref: PrefValueType,
|
||||
) => AsyncFailable<any>;
|
||||
@Input() @Required translator: {
|
||||
[key in string]: string;
|
||||
};
|
||||
@Input() @Required name: string = '';
|
||||
|
||||
@Input() helpText: string = '';
|
||||
|
||||
private updateSubject = new Subject<PrefValueType>();
|
||||
|
||||
|
@ -36,10 +36,6 @@ export class PrefOptionComponent implements OnInit {
|
|||
this.subscribeUpdate();
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.translator[this.pref.key] ?? this.pref.key;
|
||||
}
|
||||
|
||||
get valString(): string {
|
||||
if (this.pref.type !== 'string') {
|
||||
throw new Error('Not a string preference');
|
||||
|
@ -76,6 +72,10 @@ export class PrefOptionComponent implements OnInit {
|
|||
this.update(value);
|
||||
}
|
||||
|
||||
booleanUpdateWrapper(e: boolean) {
|
||||
this.update(e);
|
||||
}
|
||||
|
||||
private async updatePreference(value: PrefValueType) {
|
||||
const result = await this.updateFunction(this.pref.key, value);
|
||||
if (!HasFailed(result)) {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ErrorManagerModule } from 'src/app/util/error-manager/error-manager.module';
|
||||
import { PrefOptionComponent } from './pref-option.component';
|
||||
|
||||
|
@ -12,10 +14,12 @@ import { PrefOptionComponent } from './pref-option.component';
|
|||
CommonModule,
|
||||
ErrorManagerModule,
|
||||
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDividerModule,
|
||||
MatSlideToggleModule,
|
||||
MatSelectModule,
|
||||
],
|
||||
declarations: [PrefOptionComponent],
|
||||
exports: [PrefOptionComponent],
|
||||
|
|
|
@ -1,22 +1,83 @@
|
|||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
|
||||
export const SysPreferenceFriendlyNames: {
|
||||
[key in SysPreference]: string;
|
||||
export const SysPreferenceUI: {
|
||||
[key in SysPreference]: {
|
||||
name: string;
|
||||
helpText: string;
|
||||
category: string;
|
||||
};
|
||||
} = {
|
||||
[SysPreference.JwtSecret]: 'JWT Secret',
|
||||
[SysPreference.JwtExpiresIn]: 'JWT Expiry Time',
|
||||
[SysPreference.BCryptStrength]: 'BCrypt Strength',
|
||||
[SysPreference.JwtSecret]: {
|
||||
name: 'JWT Secret',
|
||||
helpText: 'Secret used to sign JWT authentication tokens.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
[SysPreference.JwtExpiresIn]: {
|
||||
name: 'JWT Expiry Time',
|
||||
helpText: 'Time before JWT authentication tokens expire.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
[SysPreference.BCryptStrength]: {
|
||||
name: 'BCrypt Strength',
|
||||
helpText:
|
||||
'Strength of BCrypt hashing algorithm, 10 is recommended. Reduce this if running on a low powered device.',
|
||||
category: 'Authentication',
|
||||
},
|
||||
|
||||
[SysPreference.RemoveDerivativesAfter]: 'Cached Images Expiry Time',
|
||||
[SysPreference.SaveDerivatives]: 'Cache Trancoded Images',
|
||||
[SysPreference.AllowEditing]: 'Allow images to be edited (e.g. resize)',
|
||||
[SysPreference.RemoveDerivativesAfter]: {
|
||||
name: 'Cached Images Expiry Time',
|
||||
helpText:
|
||||
'Time before cached images are deleted. This does not affect the original image. Set to 0 to disable.',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
[SysPreference.SaveDerivatives]: {
|
||||
name: 'Cache Converted Images',
|
||||
helpText:
|
||||
'Cache converted images, this will reduce the time it takes to load images. It does however use more disk space.',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
[SysPreference.AllowEditing]: {
|
||||
name: 'Allow images to be edited',
|
||||
helpText:
|
||||
'Allow images to be edited (e.g. resize, flip). Using these features will use more CPU power.',
|
||||
|
||||
[SysPreference.ConversionTimeLimit]: 'Transcode/Edit Time Limit',
|
||||
[SysPreference.ConversionMemoryLimit]: 'Transcode/Edit Memory Limit MB',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
|
||||
[SysPreference.EnableTracking]: 'Enable Ackee Web Tracking',
|
||||
[SysPreference.TrackingUrl]: 'Ackee tracking URL',
|
||||
[SysPreference.TrackingId]: 'Ackee trackign website ID',
|
||||
[SysPreference.ConversionTimeLimit]: {
|
||||
name: 'Convert/Edit Time Limit',
|
||||
helpText:
|
||||
'Time limit for converting/editing images. You may need to increase this on low powered devices.',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
[SysPreference.ConversionMemoryLimit]: {
|
||||
name: 'Convert/Edit Memory Limit MB',
|
||||
helpText:
|
||||
'Memory limit for converting/editing images. You only need to increase this if you are storing massive images.',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
|
||||
[SysPreference.EnableTelemetry]: 'Enable System Telemetry',
|
||||
[SysPreference.EnableTracking]: {
|
||||
name: 'Enable Ackee Web Tracking',
|
||||
helpText:
|
||||
'Enable tracking of the website usage using Ackee. You will need to set the tracking URL and ID.',
|
||||
category: 'Usage',
|
||||
},
|
||||
[SysPreference.TrackingUrl]: {
|
||||
name: 'Ackee tracking URL',
|
||||
helpText: 'URL of the Ackee tracking server.',
|
||||
category: 'Usage',
|
||||
},
|
||||
[SysPreference.TrackingId]: {
|
||||
name: 'Ackee trackign website ID',
|
||||
helpText: 'ID of the website to track.',
|
||||
category: 'Usage',
|
||||
},
|
||||
|
||||
[SysPreference.EnableTelemetry]: {
|
||||
name: 'Enable System Telemetry',
|
||||
helpText:
|
||||
'Enable system telemetry, this will send anonymous usage data to the developers.',
|
||||
category: 'Usage',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,3 +5,10 @@ export const UsrPreferenceFriendlyNames: {
|
|||
} = {
|
||||
[UsrPreference.KeepOriginal]: 'Keep original file',
|
||||
};
|
||||
|
||||
export const UsrPreferenceHelpText: {
|
||||
[key in UsrPreference]: string;
|
||||
} = {
|
||||
[UsrPreference.KeepOriginal]:
|
||||
'Store the original files you upload to the service, this way no data will be lost. This will also store exif data.',
|
||||
};
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<h1>Settings</h1>
|
||||
|
||||
<ng-container *ngFor="let pref of preferences | async">
|
||||
<pref-option
|
||||
[pref]="pref"
|
||||
[update]="usrPrefService.setPreference.bind(usrPrefService)"
|
||||
[translator]="translator"
|
||||
></pref-option>
|
||||
</ng-container>
|
||||
<div class="row">
|
||||
<ng-container *ngFor="let pref of preferences | async">
|
||||
<pref-option
|
||||
class="col-md-6 col-12"
|
||||
[pref]="pref"
|
||||
[update]="usrPrefService.setPreference.bind(usrPrefService)"
|
||||
[name]="getName(pref.key)"
|
||||
[helpText]="getHelpText(pref.key)"
|
||||
></pref-option>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { DecodedPref } from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { Observable } from 'rxjs';
|
||||
import { UsrPreferenceFriendlyNames } from 'src/app/i18n/usr-pref.i18n';
|
||||
import {
|
||||
UsrPreferenceFriendlyNames,
|
||||
UsrPreferenceHelpText
|
||||
} from 'src/app/i18n/usr-pref.i18n';
|
||||
import { UsrPrefService } from 'src/app/services/api/usr-pref.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './settings-general.component.html',
|
||||
})
|
||||
export class SettingsGeneralComponent {
|
||||
public translator = UsrPreferenceFriendlyNames;
|
||||
private readonly translator = UsrPreferenceFriendlyNames;
|
||||
private readonly helpTranslator = UsrPreferenceHelpText;
|
||||
|
||||
public getName(key: string) {
|
||||
return (this.translator as any)[key] ?? key;
|
||||
}
|
||||
|
||||
public getHelpText(key: string) {
|
||||
return (this.helpTranslator as any)[key] ?? '';
|
||||
}
|
||||
|
||||
preferences: Observable<DecodedPref[]>;
|
||||
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
<h1>System Settings</h1>
|
||||
|
||||
<ng-container *ngFor="let pref of preferences | async">
|
||||
<pref-option
|
||||
[pref]="pref"
|
||||
[update]="sysPrefService.setPreference.bind(sysPrefService)"
|
||||
[translator]="translator"
|
||||
></pref-option>
|
||||
<ng-container *ngFor="let category of preferences | async">
|
||||
<h2 *ngIf="category.category !== null">{{ category.category }}</h2>
|
||||
<div class="row">
|
||||
<ng-container *ngFor="let pref of category.prefs">
|
||||
<pref-option
|
||||
class="col-md-6 col-12"
|
||||
[pref]="pref"
|
||||
[update]="sysPrefService.setPreference.bind(sysPrefService)"
|
||||
[name]="getName(pref.key)"
|
||||
[helpText]="getHelpText(pref.key)"
|
||||
></pref-option>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,18 +1,42 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { DecodedPref } from 'picsur-shared/dist/dto/preferences.dto';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SysPreferenceFriendlyNames } from 'src/app/i18n/sys-pref.i18n';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { SysPreferenceUI } from 'src/app/i18n/sys-pref.i18n';
|
||||
|
||||
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||
import { SysPrefService } from 'src/app/services/api/sys-pref.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './settings-sys-pref.component.html',
|
||||
styleUrls: ['./settings-sys-pref.component.scss'],
|
||||
})
|
||||
export class SettingsSysprefComponent {
|
||||
public readonly translator = SysPreferenceFriendlyNames;
|
||||
private readonly syspreferenceUI = SysPreferenceUI;
|
||||
|
||||
preferences: Observable<DecodedPref[]>;
|
||||
public getName(key: string) {
|
||||
return this.syspreferenceUI[key as SysPreference]?.name ?? key;
|
||||
}
|
||||
|
||||
public getHelpText(key: string) {
|
||||
return this.syspreferenceUI[key as SysPreference]?.helpText ?? '';
|
||||
}
|
||||
|
||||
public getCategory(key: string): null | string {
|
||||
return this.syspreferenceUI[key as SysPreference]?.category ?? null;
|
||||
}
|
||||
|
||||
preferences: Observable<Array<{ category: string | null; prefs: DecodedPref[] }>>;
|
||||
|
||||
constructor(public readonly sysPrefService: SysPrefService) {
|
||||
this.preferences = sysPrefService.live;
|
||||
this.preferences = sysPrefService.live.pipe(
|
||||
map((prefs) => {
|
||||
const categories = makeUnique(prefs.map((pref) => this.getCategory(pref.key)));
|
||||
return categories.map((category) => ({
|
||||
category,
|
||||
prefs: prefs.filter((pref) => this.getCategory(pref.key) === category),
|
||||
}));
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@ import { SettingsSysprefRoutingModule } from './settings-sys-pref.routing.module
|
|||
|
||||
@NgModule({
|
||||
declarations: [SettingsSysprefComponent],
|
||||
imports: [CommonModule, SettingsSysprefRoutingModule, PrefOptionModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SettingsSysprefRoutingModule,
|
||||
PrefOptionModule,
|
||||
],
|
||||
})
|
||||
export default class SettingsSysprefRouteModule {}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Inject, Injectable } from '@angular/core';
|
||||
import { LOCATION, NAVIGATOR, WINDOW } from '@ng-web-apis/common';
|
||||
import { Logger } from '../logger/logger.service';
|
||||
import { InfoService } from '../api/info.service';
|
||||
import { NAVIGATOR } from '@ng-web-apis/common';
|
||||
import type { AckeeInstance, AckeeTrackingReturn } from 'ackee-tracker';
|
||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import { TrackingState } from 'picsur-shared/dist/dto/tracking-state.enum';
|
||||
import { InfoService } from '../api/info.service';
|
||||
import { Logger } from '../logger/logger.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -24,6 +24,11 @@ export class UsageService {
|
|||
this.doNotTrack =
|
||||
this.navigator.doNotTrack === '1' || this.navigator.doNotTrack === 'yes';
|
||||
|
||||
if (this.doNotTrack) {
|
||||
this.logger.verbose('Usage reporting disabled by DNT');
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribeInfo();
|
||||
}
|
||||
|
||||
|
@ -37,8 +42,7 @@ export class UsageService {
|
|||
this.stop();
|
||||
} else {
|
||||
this.setup(
|
||||
info.tracking.state === TrackingState.Detailed &&
|
||||
this.doNotTrack === false,
|
||||
info.tracking.state === TrackingState.Detailed,
|
||||
info.tracking.id,
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue