[Issues: #I9CM5M] 解决无法获取错误回调问题
Signed-off-by: baofeng <baofeng6@h-partners.com>
This commit is contained in:
parent
acc5ac5c72
commit
651b848f91
|
@ -11,6 +11,7 @@
|
|||
- 修复复用场景下图片闪动以及概率错位
|
||||
- 获取组件宽高改为使用CanvasRenderingContext2D对象获取宽高,并修复改变字体大小导致部分图片消失
|
||||
- 修复获取不到磁盘缓存文件问题
|
||||
- 修复获取不到网络请求错误回调问题
|
||||
|
||||
## 2.1.2-rc.10
|
||||
- 修复部分gif图片识别成静态图
|
||||
|
|
|
@ -48,6 +48,7 @@ import { MemoryCacheProxy } from './requestmanage/MemoryCacheProxy'
|
|||
import { ObjectKey } from './ObjectKey'
|
||||
import { TaskParams } from './TaskParams'
|
||||
import { Constants } from './constants/Constants'
|
||||
import { TransformUtils } from './transform/TransformUtils'
|
||||
|
||||
export class ImageKnife {
|
||||
static readonly SEPARATOR: string = '/'
|
||||
|
@ -604,6 +605,7 @@ export class ImageKnife {
|
|||
if ((typeof (data as PixelMap).isEditable) == 'boolean') {
|
||||
let imageKnifeData = ImageKnifeData.createImagePixelMap(ImageKnifeType.PIXELMAP, data as PixelMap);
|
||||
request.placeholderOnComplete(imageKnifeData)
|
||||
this.memoryCacheProxy.putValue(request.placeholderCacheKey,imageKnifeData)
|
||||
} else {
|
||||
request.placeholderOnError("request placeholder error")
|
||||
}
|
||||
|
@ -611,6 +613,7 @@ export class ImageKnife {
|
|||
if ((typeof (data as PixelMap).isEditable) == 'boolean') {
|
||||
let imageKnifeData = ImageKnifeData.createImagePixelMap(ImageKnifeType.PIXELMAP, data as PixelMap);
|
||||
request.retryholderOnComplete(imageKnifeData)
|
||||
this.memoryCacheProxy.putValue(request.retryholderCacheKey,imageKnifeData)
|
||||
} else {
|
||||
request.retryholderOnError("request retryholder error")
|
||||
}
|
||||
|
@ -618,6 +621,7 @@ export class ImageKnife {
|
|||
if ((typeof (data as PixelMap).isEditable) == 'boolean') {
|
||||
let imageKnifeData = ImageKnifeData.createImagePixelMap(ImageKnifeType.PIXELMAP, data as PixelMap);
|
||||
request.errorholderOnComplete(imageKnifeData)
|
||||
this.memoryCacheProxy.putValue(request.errorholderCacheKey,imageKnifeData)
|
||||
} else {
|
||||
request.errorholderOnError("request errorholder error")
|
||||
}
|
||||
|
@ -626,10 +630,14 @@ export class ImageKnife {
|
|||
let imageKnifeData = ImageKnifeData.createImagePixelMap(ImageKnifeType.PIXELMAP, data as PixelMap);
|
||||
imageKnifeData.needSaveDisk = true;
|
||||
request.loadComplete(imageKnifeData)
|
||||
this.memoryCacheProxy.putValue(request.generateCacheKey,imageKnifeData)
|
||||
this.setDiskCache(request)
|
||||
} else if ((data as GIFFrame[]).length > 0) {
|
||||
let imageKnifeData = ImageKnifeData.createImageGIFFrame(ImageKnifeType.GIFFRAME, data as GIFFrame[]);
|
||||
imageKnifeData.needSaveDisk = true;
|
||||
request.loadComplete(imageKnifeData)
|
||||
this.memoryCacheProxy.putValue(request.generateCacheKey,imageKnifeData)
|
||||
this.setDiskCache(request)
|
||||
} else {
|
||||
request.loadError("request resources error")
|
||||
}
|
||||
|
@ -640,6 +648,25 @@ export class ImageKnife {
|
|||
|
||||
}
|
||||
|
||||
private setDiskCache(request: RequestOption):void{
|
||||
try {
|
||||
// let diskMemoryCache = ImageKnifeGlobal.getInstance().getImageKnife()?.getDiskMemoryCache();
|
||||
let dataArraybuffer: ArrayBuffer = DiskLruCache.getFileCacheByFile(this.diskMemoryCache.getPath() as string, request.generateDataKey) as ArrayBuffer;
|
||||
//缓存原图片
|
||||
if (dataArraybuffer) {
|
||||
this.diskMemoryCache.setCacheMapAndSize(request.generateDataKey, dataArraybuffer);
|
||||
}
|
||||
//缓存变换后图片
|
||||
let resourceArraybuffer: ArrayBuffer = DiskLruCache.getFileCacheByFile(this.diskMemoryCache.getPath() as string, request.generateResourceKey) as ArrayBuffer;
|
||||
if (resourceArraybuffer) {
|
||||
this.diskMemoryCache.setCacheMapAndSize(request.generateResourceKey, resourceArraybuffer);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
LogUtil.error("imageknife DiskMemoryCache setDiskCache error :" + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private keyNotEmpty(request: RequestOption): boolean {
|
||||
if (
|
||||
request.generateCacheKey != null && request.generateCacheKey.length > 0 &&
|
||||
|
@ -778,11 +805,7 @@ async function taskExecute(taskParams: TaskParams, loadSrcJson: string): Promise
|
|||
return await new Promise<PixelMap>(manager.process);
|
||||
} else {
|
||||
if (transformations) {
|
||||
for (let i = 0; i < transformations.length; i++) {
|
||||
let className = transformations[i][0] as string;
|
||||
let params = transformations[i][1] as string;
|
||||
newRequestOption.addTransformations(className, params);
|
||||
}
|
||||
newRequestOption.setTransformations(TransformUtils.addTransformations(transformations))
|
||||
}
|
||||
let newDataFetch = new DownloadClient();
|
||||
let newResourceFetch = new ParseResClient();
|
||||
|
|
|
@ -54,7 +54,6 @@ import { BusinessError } from '@ohos.base'
|
|||
import { ObjectKey } from './ObjectKey'
|
||||
import common from '@ohos.app.ability.common'
|
||||
import { GIFFrame } from './utils/gif/GIFFrame'
|
||||
import { MemoryCacheProxy } from './requestmanage/MemoryCacheProxy'
|
||||
import { DiskCacheProxy } from './requestmanage/DiskCacheProxy'
|
||||
import { DiskLruCache } from '../cache/DiskLruCache'
|
||||
import { SparkMD5 } from '../3rd_party/sparkmd5/spark-md5'
|
||||
|
@ -168,6 +167,9 @@ export class RequestOption {
|
|||
setPriority(priority: Priority) {
|
||||
this.priority = priority
|
||||
}
|
||||
setTransformations( array:Array<BaseTransform<PixelMap>>){
|
||||
this.transformations = array;
|
||||
}
|
||||
generateUUID(): string {
|
||||
let d = new Date().getTime();
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(new RegExp("[xy]", "g"), (c) => {
|
||||
|
@ -478,7 +480,6 @@ export class RequestOption {
|
|||
placeholderOnComplete = (imageKnifeData:ImageKnifeData) => {
|
||||
LogUtil.log("placeholderOnComplete has called!");
|
||||
LogUtil.log("Main Image is Ready:" + this.loadMainReady);
|
||||
this.setMemoryCache(imageKnifeData,this.placeholderCacheKey);
|
||||
if (!this.loadMainReady && !(this.loadErrorReady || this.loadRetryReady) && !this.loadThumbnailReady) {
|
||||
// 主图未加载成功,并且未加载失败 显示占位图 主图加载成功或者加载失败后=>不展示占位图
|
||||
if (this.placeholderSrc != undefined) {
|
||||
|
@ -511,7 +512,6 @@ export class RequestOption {
|
|||
}
|
||||
// 加载失败 占位图解析成功
|
||||
errorholderOnComplete = (imageKnifeData:ImageKnifeData) => {
|
||||
this.setMemoryCache(imageKnifeData,this.errorholderCacheKey);
|
||||
// 如果有错误占位图 先解析并保存在RequestOption中 等到加载失败时候进行调用
|
||||
this.errorholderData = imageKnifeData;
|
||||
if (this.loadErrorReady) {
|
||||
|
@ -525,7 +525,6 @@ export class RequestOption {
|
|||
LogUtil.log("失败占位图解析失败 error =" + error)
|
||||
}
|
||||
retryholderOnComplete = (imageKnifeData:ImageKnifeData) => {
|
||||
this.setMemoryCache(imageKnifeData,this.retryholderCacheKey);
|
||||
this.retryholderData = imageKnifeData;
|
||||
if (this.loadRetryReady) {
|
||||
if (this.retryholderFunc != undefined) {
|
||||
|
@ -536,14 +535,9 @@ export class RequestOption {
|
|||
retryholderOnError = (error: BusinessError | string) => {
|
||||
LogUtil.log("重试占位图解析失败 error =" + error)
|
||||
}
|
||||
|
||||
loadComplete = (imageKnifeData: ImageKnifeData) => {
|
||||
this.setMemoryCache(imageKnifeData,this.generateCacheKey);
|
||||
if (typeof this.loadSrc == 'string' && imageKnifeData.needSaveDisk) {
|
||||
this.setDiskCache();
|
||||
}
|
||||
|
||||
this.loadMainReady = true;
|
||||
|
||||
// 三级缓存数据加载成功
|
||||
if (this.requestListeners != undefined) {
|
||||
for (let i = 0; i < this.requestListeners.length; i++) {
|
||||
|
@ -592,32 +586,7 @@ export class RequestOption {
|
|||
}
|
||||
}
|
||||
}
|
||||
//设置内存缓存
|
||||
setMemoryCache = (imageKnifeData: ImageKnifeData,cacheKey:string) => {
|
||||
|
||||
let memoryCacheProxy = ImageKnifeGlobal.getInstance()
|
||||
.getImageKnife()?.getMemoryCacheProxy() as MemoryCacheProxy<string, ImageKnifeData>;
|
||||
memoryCacheProxy.putValue(cacheKey, imageKnifeData);
|
||||
}
|
||||
//设置磁盘缓存
|
||||
setDiskCache = () => {
|
||||
try {
|
||||
let diskMemoryCache = ImageKnifeGlobal.getInstance().getImageKnife()?.getDiskMemoryCache();
|
||||
let dataArraybuffer: ArrayBuffer = DiskLruCache.getFileCacheByFile(diskMemoryCache?.getPath() as string, this.generateDataKey) as ArrayBuffer;
|
||||
//缓存原图片
|
||||
if (dataArraybuffer) {
|
||||
diskMemoryCache?.setCacheMapAndSize(this.generateDataKey, dataArraybuffer);
|
||||
}
|
||||
//缓存变换后图片
|
||||
let resourceArraybuffer: ArrayBuffer = DiskLruCache.getFileCacheByFile(diskMemoryCache?.getPath() as string, this.generateResourceKey) as ArrayBuffer;
|
||||
if (resourceArraybuffer) {
|
||||
diskMemoryCache?.setCacheMapAndSize(this.generateResourceKey, resourceArraybuffer);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
LogUtil.error("imageknife DiskMemoryCache setDiskCache error :" + e.message);
|
||||
}
|
||||
}
|
||||
// 图片文件落盘之后会自动去寻找下一个数据加载
|
||||
removeCurrentAndSearchNext = () => {
|
||||
if (ImageKnifeGlobal.getInstance().getImageKnife() != undefined) {
|
||||
|
@ -642,100 +611,21 @@ export class RequestOption {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.requestListeners != undefined) {
|
||||
for (let i = 0; i < this.requestListeners.length; i++) {
|
||||
let requestListener = this.requestListeners[i];
|
||||
let boolInterception = requestListener.callback(err as string, new ImageKnifeData());
|
||||
if (boolInterception) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 加载失败之后
|
||||
if (ImageKnifeGlobal.getInstance().getImageKnife() != undefined) {
|
||||
(ImageKnifeGlobal.getInstance().getImageKnife())?.removeRunning(this);
|
||||
}
|
||||
}
|
||||
addTransformations = (transformationName: string, params: string) => {
|
||||
switch (transformationName) {
|
||||
case "BlurTransformation":
|
||||
let paramList: number [] = JSON.parse(params);
|
||||
this.transformations.push(new BlurTransformation(paramList[0], paramList[1]));
|
||||
break;
|
||||
case "BrightnessFilterTransformation":
|
||||
let paramList1: number [] = JSON.parse(params);
|
||||
this.transformations.push(new BrightnessFilterTransformation(paramList1[0]));
|
||||
break;
|
||||
case "RotateImageTransformation":
|
||||
let paramList2: number [] = JSON.parse(params);
|
||||
this.transformations.push(new RotateImageTransformation(paramList2[0]));
|
||||
break;
|
||||
case "GrayscaleTransformation":
|
||||
this.transformations.push(new GrayscaleTransformation());
|
||||
break;
|
||||
case "BlurTransformation":
|
||||
let paramList3: number [] = JSON.parse(params);
|
||||
this.transformations.push(new BlurTransformation(paramList3[0]));
|
||||
break;
|
||||
case "ContrastFilterTransformation":
|
||||
let paramList4: number [] = JSON.parse(params);
|
||||
this.transformations.push(new ContrastFilterTransformation(paramList4[0]));
|
||||
break;
|
||||
case "CropCircleTransformation":
|
||||
this.transformations.push(new CropCircleTransformation());
|
||||
break;
|
||||
case "CropCircleWithBorderTransformation":
|
||||
let paramList5: (number | rgbColor) [] = JSON.parse(params);
|
||||
this.transformations.push(new CropCircleWithBorderTransformation(paramList5[0] as number, paramList5[1] as rgbColor));
|
||||
break;
|
||||
case "CropSquareTransformation":
|
||||
this.transformations.push(new CropSquareTransformation());
|
||||
break;
|
||||
case "InvertFilterTransformation":
|
||||
this.transformations.push(new InvertFilterTransformation());
|
||||
break;
|
||||
case "KuwaharaFilterTransform":
|
||||
let paramList7: number [] = JSON.parse(params);
|
||||
this.transformations.push(new KuwaharaFilterTransform(paramList7[0] as number));
|
||||
break;
|
||||
case "MaskTransformation":
|
||||
let paramList8: Resource [] = JSON.parse(params);
|
||||
this.transformations.push(new MaskTransformation(paramList8[0] as Resource));
|
||||
break;
|
||||
case "PixelationFilterTransformation":
|
||||
let paramList9: number [] = JSON.parse(params);
|
||||
this.transformations.push(new PixelationFilterTransformation(paramList9[0] as number));
|
||||
break;
|
||||
case "RoundedCornersTransformation":
|
||||
let paramList10: RoundCorner [] = JSON.parse(params);
|
||||
this.transformations.push(new RoundedCornersTransformation(paramList10[0] as RoundCorner));
|
||||
break;
|
||||
case "SepiaFilterTransformation":
|
||||
this.transformations.push(new SepiaFilterTransformation());
|
||||
break;
|
||||
case "SketchFilterTransformation":
|
||||
this.transformations.push(new SketchFilterTransformation());
|
||||
break;
|
||||
case "SwirlFilterTransformation":
|
||||
let paramList11: (number | Array<number>) [] = JSON.parse(params);
|
||||
this.transformations.push(new SwirlFilterTransformation(paramList11[0] as number, paramList11[1] as number, paramList11[2] as Array<number>));
|
||||
break;
|
||||
case "ToonFilterTransform":
|
||||
let paramList14: number [] = JSON.parse(params);
|
||||
this.transformations.push(new ToonFilterTransform(paramList14[0], paramList14[1]));
|
||||
break;
|
||||
case "VignetteFilterTransform":
|
||||
let paramList12: Array<number> [] = JSON.parse(params);
|
||||
this.transformations.push(new VignetteFilterTransform(paramList12[0], paramList12[1], paramList12[2]));
|
||||
break;
|
||||
case "CenterCrop":
|
||||
this.transformations.push(new CenterCrop());
|
||||
break;
|
||||
case "CenterInside":
|
||||
this.transformations.push(new CenterInside());
|
||||
break;
|
||||
case "FitCenter":
|
||||
this.transformations.push(new FitCenter());
|
||||
break;
|
||||
case "CropTransformation":
|
||||
let paramList13: (number | CropType) [] = JSON.parse(params);
|
||||
this.transformations.push(new CropTransformation(paramList13[0] as number, paramList13[1] as number, paramList13[2] as CropType));
|
||||
break;
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -71,10 +71,10 @@ export class HttpDownloadClient implements IDataFetch {
|
|||
if (data == 200) {
|
||||
|
||||
} else {
|
||||
onError(`HttpDownloadClient has error, http code = ${data}`)
|
||||
onError(`HttpDownloadClient has error, http code = ` + JSON.stringify(data))
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
onError(`HttpDownloadClient has error, http code = ${err}`)
|
||||
onError(`HttpDownloadClient has error, http code = ` + JSON.stringify(err))
|
||||
})
|
||||
} catch (err) {
|
||||
onError('HttpDownloadClient catch err request uuid =' + request.uuid)
|
||||
|
|
|
@ -17,6 +17,29 @@ import image from '@ohos.multimedia.image'
|
|||
import { Size } from '../../imageknife/RequestOption'
|
||||
import { BusinessError } from '@ohos.base'
|
||||
import { Constants } from '../constants/Constants'
|
||||
import { BaseTransform } from './BaseTransform'
|
||||
import { BlurTransformation } from './BlurTransformation'
|
||||
import { BrightnessFilterTransformation } from './BrightnessFilterTransformation'
|
||||
import { RotateImageTransformation } from './RotateImageTransformation'
|
||||
import { GrayscaleTransformation } from './GrayscaleTransformation'
|
||||
import { ContrastFilterTransformation } from './ContrastFilterTransformation'
|
||||
import { CropCircleTransformation } from './CropCircleTransformation'
|
||||
import { CropCircleWithBorderTransformation, rgbColor } from './CropCircleWithBorderTransformation'
|
||||
import { CropSquareTransformation } from './CropSquareTransformation'
|
||||
import { InvertFilterTransformation } from './InvertFilterTransformation'
|
||||
import { KuwaharaFilterTransform } from './KuwaharaFilterTransform'
|
||||
import { MaskTransformation } from './MaskTransformation'
|
||||
import { PixelationFilterTransformation } from './PixelationFilterTransformation'
|
||||
import { RoundCorner, RoundedCornersTransformation } from './RoundedCornersTransformation'
|
||||
import { SketchFilterTransformation } from './SketchFilterTransformation'
|
||||
import { SepiaFilterTransformation } from './SepiaFilterTransformation'
|
||||
import { SwirlFilterTransformation } from './SwirlFilterTransformation'
|
||||
import { ToonFilterTransform } from './ToonFilterTransform'
|
||||
import { VignetteFilterTransform } from './VignetteFilterTransform'
|
||||
import { CenterCrop } from './pixelmap/CenterCrop'
|
||||
import { CenterInside } from './pixelmap/CenterInside'
|
||||
import { FitCenter } from './pixelmap/FitCenter'
|
||||
import { CropTransformation, CropType } from './CropTransformation'
|
||||
|
||||
export class TransformUtils {
|
||||
static centerCrop(buf: ArrayBuffer, outWidth: number, outHeihgt: number,
|
||||
|
@ -155,4 +178,100 @@ export class TransformUtils {
|
|||
func?.asyncTransform('', { width: pWidth, height: pHeight });
|
||||
})
|
||||
}
|
||||
|
||||
static addTransformations (data: string [][]) {
|
||||
let transformations: Array<BaseTransform<PixelMap>> = new Array();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let className = data[i][0] as string;
|
||||
let params = data[i][1] as string;
|
||||
switch (className) {
|
||||
case "BlurTransformation":
|
||||
let paramList: number [] = JSON.parse(params);
|
||||
transformations.push(new BlurTransformation(paramList[0], paramList[1]));
|
||||
break;
|
||||
case "BrightnessFilterTransformation":
|
||||
let paramList1: number [] = JSON.parse(params);
|
||||
transformations.push(new BrightnessFilterTransformation(paramList1[0]));
|
||||
break;
|
||||
case "RotateImageTransformation":
|
||||
let paramList2: number [] = JSON.parse(params);
|
||||
transformations.push(new RotateImageTransformation(paramList2[0]));
|
||||
break;
|
||||
case "GrayscaleTransformation":
|
||||
transformations.push(new GrayscaleTransformation());
|
||||
break;
|
||||
case "BlurTransformation":
|
||||
let paramList3: number [] = JSON.parse(params);
|
||||
transformations.push(new BlurTransformation(paramList3[0]));
|
||||
break;
|
||||
case "ContrastFilterTransformation":
|
||||
let paramList4: number [] = JSON.parse(params);
|
||||
transformations.push(new ContrastFilterTransformation(paramList4[0]));
|
||||
break;
|
||||
case "CropCircleTransformation":
|
||||
transformations.push(new CropCircleTransformation());
|
||||
break;
|
||||
case "CropCircleWithBorderTransformation":
|
||||
let paramList5: (number | rgbColor) [] = JSON.parse(params);
|
||||
transformations.push(new CropCircleWithBorderTransformation(paramList5[0] as number, paramList5[1] as rgbColor));
|
||||
break;
|
||||
case "CropSquareTransformation":
|
||||
transformations.push(new CropSquareTransformation());
|
||||
break;
|
||||
case "InvertFilterTransformation":
|
||||
transformations.push(new InvertFilterTransformation());
|
||||
break;
|
||||
case "KuwaharaFilterTransform":
|
||||
let paramList7: number [] = JSON.parse(params);
|
||||
transformations.push(new KuwaharaFilterTransform(paramList7[0] as number));
|
||||
break;
|
||||
case "MaskTransformation":
|
||||
let paramList8: Resource [] = JSON.parse(params);
|
||||
transformations.push(new MaskTransformation(paramList8[0] as Resource));
|
||||
break;
|
||||
case "PixelationFilterTransformation":
|
||||
let paramList9: number [] = JSON.parse(params);
|
||||
transformations.push(new PixelationFilterTransformation(paramList9[0] as number));
|
||||
break;
|
||||
case "RoundedCornersTransformation":
|
||||
let paramList10: RoundCorner [] = JSON.parse(params);
|
||||
transformations.push(new RoundedCornersTransformation(paramList10[0] as RoundCorner));
|
||||
break;
|
||||
case "SepiaFilterTransformation":
|
||||
transformations.push(new SepiaFilterTransformation());
|
||||
break;
|
||||
case "SketchFilterTransformation":
|
||||
transformations.push(new SketchFilterTransformation());
|
||||
break;
|
||||
case "SwirlFilterTransformation":
|
||||
let paramList11: (number | Array<number>) [] = JSON.parse(params);
|
||||
transformations.push(new SwirlFilterTransformation(paramList11[0] as number, paramList11[1] as number, paramList11[2] as Array<number>));
|
||||
break;
|
||||
case "ToonFilterTransform":
|
||||
let paramList14: number [] = JSON.parse(params);
|
||||
transformations.push(new ToonFilterTransform(paramList14[0], paramList14[1]));
|
||||
break;
|
||||
case "VignetteFilterTransform":
|
||||
let paramList12: Array<number> [] = JSON.parse(params);
|
||||
transformations.push(new VignetteFilterTransform(paramList12[0], paramList12[1], paramList12[2]));
|
||||
break;
|
||||
case "CenterCrop":
|
||||
transformations.push(new CenterCrop());
|
||||
break;
|
||||
case "CenterInside":
|
||||
transformations.push(new CenterInside());
|
||||
break;
|
||||
case "FitCenter":
|
||||
transformations.push(new FitCenter());
|
||||
break;
|
||||
case "CropTransformation":
|
||||
let paramList13: (number | CropType) [] = JSON.parse(params);
|
||||
transformations.push(new CropTransformation(paramList13[0] as number, paramList13[1] as number, paramList13[2] as CropType));
|
||||
break;
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return transformations;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue