State management & UI improvements
This commit is contained in:
32
ui.html
32
ui.html
@@ -7,6 +7,24 @@
|
|||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<style>
|
<style>
|
||||||
|
.center-parent {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#capture {
|
||||||
|
width: 20ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#config {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
#config label {
|
#config label {
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
}
|
}
|
||||||
@@ -14,11 +32,6 @@
|
|||||||
#config select {
|
#config select {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas-holder,
|
|
||||||
#config {
|
|
||||||
max-height: 100vh;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
@@ -31,12 +44,5 @@
|
|||||||
</script>
|
</script>
|
||||||
<script type="module" src="ui.js"></script>
|
<script type="module" src="ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body></body>
|
||||||
<div class="pure-g">
|
|
||||||
<div class="pure-u-2-3" id="canvas-holder">
|
|
||||||
<canvas id="canvas" style="display: flex; margin: auto"></canvas>
|
|
||||||
</div>
|
|
||||||
<div id="config" class="pure-u-1-3" style="overflow-y: auto"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
121
ui.js
121
ui.js
@@ -9,16 +9,21 @@ if (new URLSearchParams(location.search).has('debug')) {
|
|||||||
await import('preact/debug');
|
await import('preact/debug');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let prepareContext;
|
||||||
|
|
||||||
/** Schedules an exclusive async operation on the global context. */
|
/** Schedules an exclusive async operation on the global context. */
|
||||||
const scheduleOp = (() => {
|
const scheduleOp = (() => {
|
||||||
let queue = initModule()
|
let queue = (async () => {
|
||||||
.then(Module => new Module.Context())
|
await new Promise(resolve => {
|
||||||
.then(ctx => {
|
prepareContext = resolve;
|
||||||
|
});
|
||||||
|
let { Context } = await initModule();
|
||||||
|
let ctx = await new Context();
|
||||||
addEventListener('beforeunload', e => {
|
addEventListener('beforeunload', e => {
|
||||||
ctx.delete();
|
ctx.delete();
|
||||||
});
|
});
|
||||||
return ctx;
|
return ctx;
|
||||||
});
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
@@ -231,9 +236,9 @@ class CaptureButton extends Component {
|
|||||||
null,
|
null,
|
||||||
h('input', {
|
h('input', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
id: 'capture',
|
||||||
class:
|
class:
|
||||||
'pure-input-1-3 pure-button' +
|
'pure-button' + (this.state.inProgress ? ' pure-button-active' : ''),
|
||||||
(this.state.inProgress ? ' pure-button-active' : ''),
|
|
||||||
onclick: this.handleCapture,
|
onclick: this.handleCapture,
|
||||||
value: `${this.state.inProgress ? '⌛' : '📷'} Capture image`
|
value: `${this.state.inProgress ? '⌛' : '📷'} Capture image`
|
||||||
})
|
})
|
||||||
@@ -241,56 +246,106 @@ class CaptureButton extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @extends Component<null, { config: string | Config }> */
|
/** @typedef {{ type: 'Initial' } | { type: 'Status', message: string } | { type: 'Config', config: Config }} AppState */
|
||||||
class Settings extends Component {
|
|
||||||
state = { config: '' };
|
|
||||||
|
|
||||||
constructor() {
|
/** @extends Component<null, AppState> */
|
||||||
super();
|
class App extends Component {
|
||||||
|
state = /** @type {AppState} */ ({ type: 'Initial' });
|
||||||
|
|
||||||
|
selectDevice = async () => {
|
||||||
|
await navigator.usb.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
classCode: 6, // PTP
|
||||||
|
subclassCode: 1 // MTP
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
prepareContext();
|
||||||
|
this.setState({ type: 'Status', message: 'Connecting...' });
|
||||||
(async () => {
|
(async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
await this.refreshConfig();
|
await this.refreshConfig();
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
};
|
||||||
|
|
||||||
async refreshConfig() {
|
async refreshConfig() {
|
||||||
let config;
|
|
||||||
try {
|
try {
|
||||||
config = await scheduleOp(context => context.configToJS());
|
|
||||||
} catch (e) {
|
|
||||||
config = String(e);
|
|
||||||
}
|
|
||||||
this.setState({
|
this.setState({
|
||||||
config
|
type: 'Config',
|
||||||
|
config: await scheduleOp(context => context.configToJS())
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
type: 'Status',
|
||||||
|
message: String(e)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(/** @type {App['props']} */ props, /** @type {App['state']} */ state) {
|
||||||
/** @type {Settings['props']} */ props,
|
switch (state.type) {
|
||||||
/** @type {Settings['state']} */ state
|
case 'Initial':
|
||||||
) {
|
return h(
|
||||||
return typeof state.config === 'string'
|
'div',
|
||||||
? state.config
|
{ class: 'center-parent' },
|
||||||
: h(
|
h('input', {
|
||||||
|
class: 'center',
|
||||||
|
type: 'button',
|
||||||
|
onclick: this.selectDevice,
|
||||||
|
value: '🔍 Select camera'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
case 'Status':
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{ class: 'center-parent' },
|
||||||
|
h('div', { class: 'center' }, state.message)
|
||||||
|
);
|
||||||
|
case 'Config':
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{ class: 'pure-g' },
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'pure-u-2-3 center-parent', id: 'canvas-holder' },
|
||||||
|
h(PreviewCanvas, {
|
||||||
|
id: 'preview-canvas',
|
||||||
|
class: 'center'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ id: 'config', class: 'pure-u-1-3' },
|
||||||
|
h(
|
||||||
'form',
|
'form',
|
||||||
{ class: 'pure-form pure-form-aligned' },
|
{ class: 'pure-form pure-form-aligned' },
|
||||||
h(CaptureButton, null),
|
h(CaptureButton, null),
|
||||||
h(Widget, { config: state.config })
|
h(Widget, { config: state.config })
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render(h(Settings, null), document.getElementById('config'));
|
render(h(App, null), document.body);
|
||||||
|
|
||||||
(async () => {
|
class PreviewCanvas extends Component {
|
||||||
|
ref = createRef();
|
||||||
|
|
||||||
|
render(props) {
|
||||||
|
return h('canvas', { ...props, ref: this.ref });
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
// I have no idea why, but if we connect too soon, it just hangs...
|
// I have no idea why, but if we connect too soon, it just hangs...
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
let canvas = /** @type {HTMLCanvasElement} */ (
|
let canvas = /** @type {HTMLCanvasElement} */ (this.ref.current);
|
||||||
document.getElementById('canvas')
|
let canvasHolder = canvas.parentElement;
|
||||||
);
|
|
||||||
|
|
||||||
let canvasCtx = canvas.getContext('bitmaprenderer');
|
let canvasCtx = canvas.getContext('bitmaprenderer');
|
||||||
|
|
||||||
@@ -313,7 +368,6 @@ render(h(Settings, null), document.getElementById('config'));
|
|||||||
);
|
);
|
||||||
if (!ratio) {
|
if (!ratio) {
|
||||||
ratio = img.width / img.height;
|
ratio = img.width / img.height;
|
||||||
let canvasHolder = document.getElementById('canvas-holder');
|
|
||||||
|
|
||||||
function updateCanvasSize() {
|
function updateCanvasSize() {
|
||||||
let width = canvasHolder.offsetWidth - 10;
|
let width = canvasHolder.offsetWidth - 10;
|
||||||
@@ -337,4 +391,5 @@ render(h(Settings, null), document.getElementById('config'));
|
|||||||
}
|
}
|
||||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user