From 9a7705701ea60dd407af8284f72b4e57f8272e04 Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 29 Feb 2024 14:50:10 +0300 Subject: [PATCH 1/6] feat: add `cameraCapture` option to specify inpit capture attribute value --- abstract/UploaderBlock.js | 1 + blocks/Config/initialConfig.js | 1 + blocks/Config/normalizeConfigValue.js | 10 ++++++++++ types/exported.d.ts | 1 + 4 files changed, 13 insertions(+) diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index 64a8b69d8..8a798651a 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -292,6 +292,7 @@ export class UploaderBlock extends ActivityBlock { this.fileInput.type = 'file'; this.fileInput.multiple = this.cfg.multiple; if (options.captureCamera) { + this.fileInput.capture = this.cfg.cameraCapture; this.fileInput.capture = ''; this.fileInput.accept = serializeCsv(IMAGE_ACCEPT_LIST); } else { diff --git a/blocks/Config/initialConfig.js b/blocks/Config/initialConfig.js index 33b6a6f1f..f834b7485 100644 --- a/blocks/Config/initialConfig.js +++ b/blocks/Config/initialConfig.js @@ -20,6 +20,7 @@ export const initialConfig = { externalSourcesPreferredTypes: '', store: 'auto', cameraMirror: false, + cameraCapture: '', sourceList: 'local, url, camera, dropbox, gdrive', cloudImageEditorTabs: serializeCsv(ALL_TABS), maxLocalFileSizeBytes: 0, diff --git a/blocks/Config/normalizeConfigValue.js b/blocks/Config/normalizeConfigValue.js index e238f4400..835e234d0 100644 --- a/blocks/Config/normalizeConfigValue.js +++ b/blocks/Config/normalizeConfigValue.js @@ -27,6 +27,15 @@ export const asBoolean = (value) => { /** @param {unknown} value */ const asStore = (value) => (value === 'auto' ? value : asBoolean(value)); +/** @param {unknown} value */ +const asCameraCapture = (value) => { + const strValue = asString(value); + if (strValue !== 'user' && strValue !== 'environment' && strValue !== '') { + throw new Error(`Invalid "cameraCapture" value: "${strValue}"`); + } + return strValue; +}; + /** * @type {{ * [Key in keyof import('../../types').ConfigPlainType]: ( @@ -46,6 +55,7 @@ const mapping = { externalSourcesPreferredTypes: asString, store: asStore, cameraMirror: asBoolean, + cameraCapture: asCameraCapture, sourceList: asString, maxLocalFileSizeBytes: asNumber, thumbSize: asNumber, diff --git a/types/exported.d.ts b/types/exported.d.ts index 1bde9d33c..d23a3bd48 100644 --- a/types/exported.d.ts +++ b/types/exported.d.ts @@ -15,6 +15,7 @@ export type ConfigType = { externalSourcesPreferredTypes: string; store: boolean | 'auto'; cameraMirror: boolean; + cameraCapture: 'user' | 'environment' | ''; sourceList: string; maxLocalFileSizeBytes: number; thumbSize: number; From 5ecacba610143f1336b2fde2b6cf67a7f17c1edb Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 29 Feb 2024 14:51:20 +0300 Subject: [PATCH 2/6] fix: specify camera input accept attribute value as simple `image/*` to prevent OS to show unrelated sources (video/audio) --- abstract/UploaderBlock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index 8a798651a..cb7ea00bb 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -293,8 +293,7 @@ export class UploaderBlock extends ActivityBlock { this.fileInput.multiple = this.cfg.multiple; if (options.captureCamera) { this.fileInput.capture = this.cfg.cameraCapture; - this.fileInput.capture = ''; - this.fileInput.accept = serializeCsv(IMAGE_ACCEPT_LIST); + this.fileInput.accept = 'image/*'; } else { this.fileInput.accept = accept; } From 95f02872f2e4b60bf7e4558b28cceb25d727a346 Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 29 Feb 2024 14:52:00 +0300 Subject: [PATCH 3/6] fix: shnow camera system dialog when camera is the only source --- abstract/UploaderBlock.js | 35 +++++++-------- blocks/SourceBtn/SourceBtn.js | 84 +++++++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index cb7ea00bb..b8eaad0e0 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -21,7 +21,6 @@ import { TypedCollection } from './TypedCollection.js'; import { buildOutputCollectionState } from './buildOutputCollectionState.js'; import { uploadEntrySchema } from './uploadEntrySchema.js'; import { throttle } from '../blocks/utils/throttle.js'; - export class UploaderBlock extends ActivityBlock { couldBeCtxOwner = false; isCtxOwner = false; @@ -300,7 +299,9 @@ export class UploaderBlock extends ActivityBlock { this.fileInput.dispatchEvent(new MouseEvent('click')); this.fileInput.onchange = () => { // @ts-ignore TODO: fix this - [...this.fileInput['files']].forEach((file) => this.addFileFromObject(file, { source: UploadSource.LOCAL })); + [...this.fileInput['files']].forEach((file) => + this.addFileFromObject(file, { source: options.captureCamera ? UploadSource.CAMERA : UploadSource.LOCAL }), + ); // To call uploadTrigger UploadList should draw file items first: this.$['*currentActivity'] = ActivityBlock.activities.UPLOAD_LIST; this.setOrAddState('*modalActive', true); @@ -330,24 +331,18 @@ export class UploaderBlock extends ActivityBlock { this.setOrAddState('*modalActive', true); } else { if (this.sourceList?.length === 1) { - let srcKey = this.sourceList[0]; - // Single source case: - if (srcKey === 'local') { - this.$['*currentActivity'] = ActivityBlock.activities.UPLOAD_LIST; - this?.['openSystemDialog'](); - } else { - if (Object.values(UploaderBlock.extSrcList).includes(/** @type {any} */ (srcKey))) { - this.set$({ - '*currentActivityParams': { - externalSourceType: srcKey, - }, - '*currentActivity': ActivityBlock.activities.EXTERNAL, - }); - } else { - this.$['*currentActivity'] = srcKey; - } - this.setOrAddState('*modalActive', true); - } + const srcKey = this.sourceList[0]; + /** @type {Set} */ + const blocksRegistry = this.$['*blocksRegistry']; + /** + * @param {import('./Block').Block} block + * @returns {block is import('../blocks/SourceBtn/SourceBtn.js').SourceBtn} + */ + const isSourceBtn = (block) => 'type' in block && block.type === srcKey; + const sourceBtnBlock = [...blocksRegistry].find(isSourceBtn); + // TODO: This is weird that we have this logic inside UI component, we should consider to move it somewhere else + sourceBtnBlock?.activate(); + this.setOrAddState('*modalActive', true); } else { // Multiple sources case: this.set$({ diff --git a/blocks/SourceBtn/SourceBtn.js b/blocks/SourceBtn/SourceBtn.js index a4d357efd..470dff8ba 100644 --- a/blocks/SourceBtn/SourceBtn.js +++ b/blocks/SourceBtn/SourceBtn.js @@ -1,23 +1,45 @@ +// @ts-check import { UploaderBlock } from '../../abstract/UploaderBlock.js'; import { ActivityBlock } from '../../abstract/ActivityBlock.js'; const L10N_PREFIX = 'src-type-'; +/** + * @typedef {{ + * type: string; + * activity?: string; + * textKey?: string; + * icon?: string; + * handle?: () => boolean; + * activityParams?: Record; + * }} TConfig + */ + export class SourceBtn extends UploaderBlock { couldBeCtxOwner = true; - /** @private */ + /** @type {string | undefined} */ + type = undefined; + /** + * @private + * @type {Record} + */ _registeredTypes = {}; - init$ = { - ...this.init$, - iconName: 'default', - }; + constructor() { + super(); + + this.init$ = { + ...this.init$, + iconName: 'default', + }; + } initTypes() { this.registerType({ type: UploaderBlock.sourceTypes.LOCAL, - onClick: () => { + handle: () => { this.openSystemDialog(); + return false; }, }); this.registerType({ @@ -28,9 +50,8 @@ export class SourceBtn extends UploaderBlock { this.registerType({ type: UploaderBlock.sourceTypes.CAMERA, activity: ActivityBlock.activities.CAMERA, - onClick: () => { - let el = document.createElement('input'); - var supportsCapture = el.capture !== undefined; + handle: () => { + const supportsCapture = 'capture' in document.createElement('input'); if (supportsCapture) { this.openSystemDialog({ captureCamera: true }); } @@ -59,39 +80,55 @@ export class SourceBtn extends UploaderBlock { this.initTypes(); this.setAttribute('role', 'button'); - this.defineAccessor('type', (val) => { - if (!val) { - return; - } - this.applyType(val); - }); + this.defineAccessor( + 'type', + /** @param {string} val */ + (val) => { + if (!val) { + return; + } + this.applyType(val); + }, + ); } + /** @param {TConfig} typeConfig */ registerType(typeConfig) { this._registeredTypes[typeConfig.type] = typeConfig; } + /** @param {string} type */ getType(type) { return this._registeredTypes[type]; } + activate() { + if (!this.type) { + return; + } + const configType = this._registeredTypes[this.type]; + const { activity, handle, activityParams = {} } = configType; + const showActivity = handle ? handle() : !!activity; + showActivity && + this.set$({ + '*currentActivityParams': activityParams, + '*currentActivity': activity, + }); + } + + /** @param {string} type */ applyType(type) { const configType = this._registeredTypes[type]; if (!configType) { console.warn('Unsupported source type: ' + type); return; } - const { textKey = type, icon = type, activity, onClick, activityParams = {} } = configType; + const { textKey = type, icon = type } = configType; this.applyL10nKey('src-type', `${L10N_PREFIX}${textKey}`); this.$.iconName = icon; - this.onclick = (e) => { - const showActivity = onClick ? onClick(e) : !!activity; - showActivity && - this.set$({ - '*currentActivityParams': activityParams, - '*currentActivity': activity, - }); + this.onclick = () => { + this.activate(); }; } } @@ -100,5 +137,6 @@ SourceBtn.template = /* HTML */ `
`; SourceBtn.bindAttributes({ + // @ts-expect-error symbiote types bug type: null, }); From 6da4212358146c8414443fff9698a53657891f23 Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 29 Feb 2024 14:52:25 +0300 Subject: [PATCH 4/6] fix: switch camera source output format to JPEG to make it shrinkable --- blocks/CameraSource/CameraSource.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/blocks/CameraSource/CameraSource.js b/blocks/CameraSource/CameraSource.js index c5a241382..36605a5b0 100644 --- a/blocks/CameraSource/CameraSource.js +++ b/blocks/CameraSource/CameraSource.js @@ -165,18 +165,19 @@ export class CameraSource extends UploaderBlock { this._canvas.width = this.ref.video['videoWidth']; // @ts-ignore this._ctx.drawImage(this.ref.video, 0, 0); - let date = Date.now(); - let name = `camera-${date}.png`; + const date = Date.now(); + const name = `camera-${date}.jpeg`; + const format = 'image/jpeg'; this._canvas.toBlob((blob) => { let file = new File([blob], name, { lastModified: date, - type: 'image/png', + type: format, }); this.addFileFromObject(file, { source: UploadSource.CAMERA }); this.set$({ '*currentActivity': ActivityBlock.activities.UPLOAD_LIST, }); - }); + }, format); } async initCallback() { From a7191160dd10429adc92d3c20825002b37bd1d9b Mon Sep 17 00:00:00 2001 From: nd0ut Date: Thu, 29 Feb 2024 14:53:37 +0300 Subject: [PATCH 5/6] chore: little refactor --- blocks/SourceBtn/SourceBtn.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blocks/SourceBtn/SourceBtn.js b/blocks/SourceBtn/SourceBtn.js index 470dff8ba..e67288634 100644 --- a/blocks/SourceBtn/SourceBtn.js +++ b/blocks/SourceBtn/SourceBtn.js @@ -10,7 +10,7 @@ const L10N_PREFIX = 'src-type-'; * activity?: string; * textKey?: string; * icon?: string; - * handle?: () => boolean; + * activate?: () => boolean; * activityParams?: Record; * }} TConfig */ @@ -37,7 +37,7 @@ export class SourceBtn extends UploaderBlock { initTypes() { this.registerType({ type: UploaderBlock.sourceTypes.LOCAL, - handle: () => { + activate: () => { this.openSystemDialog(); return false; }, @@ -50,7 +50,7 @@ export class SourceBtn extends UploaderBlock { this.registerType({ type: UploaderBlock.sourceTypes.CAMERA, activity: ActivityBlock.activities.CAMERA, - handle: () => { + activate: () => { const supportsCapture = 'capture' in document.createElement('input'); if (supportsCapture) { this.openSystemDialog({ captureCamera: true }); @@ -107,8 +107,8 @@ export class SourceBtn extends UploaderBlock { return; } const configType = this._registeredTypes[this.type]; - const { activity, handle, activityParams = {} } = configType; - const showActivity = handle ? handle() : !!activity; + const { activity, activate, activityParams = {} } = configType; + const showActivity = activate ? activate() : !!activity; showActivity && this.set$({ '*currentActivityParams': activityParams, From b471298a41deda5dc28bc0b477b8fc7ea611b78e Mon Sep 17 00:00:00 2001 From: nd0ut Date: Mon, 4 Mar 2024 11:49:24 +0300 Subject: [PATCH 6/6] chore: remove unused dep --- abstract/UploaderBlock.js | 1 - 1 file changed, 1 deletion(-) diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index b8eaad0e0..121c406f0 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -20,7 +20,6 @@ import { uploaderBlockCtx } from './CTX.js'; import { TypedCollection } from './TypedCollection.js'; import { buildOutputCollectionState } from './buildOutputCollectionState.js'; import { uploadEntrySchema } from './uploadEntrySchema.js'; -import { throttle } from '../blocks/utils/throttle.js'; export class UploaderBlock extends ActivityBlock { couldBeCtxOwner = false; isCtxOwner = false;