mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
More feedback, modal key hook
This commit is contained in:
parent
9657cc57df
commit
3a6517bb3e
30
js/apple2.ts
30
js/apple2.ts
|
@ -32,27 +32,27 @@ import { Restorable, rom } from './types';
|
|||
import { processGamepad } from './ui/gamepad';
|
||||
|
||||
export interface Apple2Options {
|
||||
characterRom: string
|
||||
enhanced: boolean
|
||||
e: boolean
|
||||
gl: boolean
|
||||
rom: string
|
||||
canvas: HTMLCanvasElement
|
||||
tick: () => void
|
||||
characterRom: string;
|
||||
enhanced: boolean;
|
||||
e: boolean;
|
||||
gl: boolean;
|
||||
rom: string;
|
||||
canvas: HTMLCanvasElement;
|
||||
tick: () => void;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
cycles: number
|
||||
frames: number
|
||||
renderedFrames: number
|
||||
cycles: number;
|
||||
frames: number;
|
||||
renderedFrames: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
cpu: CpuState
|
||||
vm: VideoModesState
|
||||
io: Apple2IOState
|
||||
mmu?: MMUState
|
||||
ram?: RAMState[]
|
||||
cpu: CpuState;
|
||||
vm: VideoModesState;
|
||||
io: Apple2IOState;
|
||||
mmu?: MMUState;
|
||||
ram?: RAMState[];
|
||||
}
|
||||
|
||||
export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { h, FunctionalComponent } from 'preact';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import { useHotKey } from './hooks/useHotKey';
|
||||
|
||||
/**
|
||||
* Temporary JS styling while I figure out how I really want
|
||||
|
@ -100,6 +101,31 @@ export const ModalFooter: FunctionalComponent = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* ModalCloseButton component properties
|
||||
*/
|
||||
interface ModalCloseButtonProp {
|
||||
onClose: (closeBox?: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a close button and registers a global Escape key
|
||||
* hook to trigger it.
|
||||
*
|
||||
* @param onClose Close callback
|
||||
* @returns ModalClose component
|
||||
*/
|
||||
export const ModalCloseButton = ({ onClose }: ModalCloseButtonProp) => {
|
||||
const doClose = useCallback(() => onClose(true), [onClose]);
|
||||
useHotKey('Escape', doClose);
|
||||
|
||||
return (
|
||||
<button onClick={doClose} title="Close">
|
||||
{'\u2715'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* ModalHeader component properties
|
||||
*/
|
||||
|
@ -113,19 +139,13 @@ export interface ModalHeaderProps {
|
|||
*
|
||||
* @param onClose Close callback
|
||||
* @param title Modal title
|
||||
* @returns
|
||||
* @returns ModalHeader component
|
||||
*/
|
||||
export const ModalHeader = ({ onClose, title }: ModalHeaderProps) => {
|
||||
const doClose = useCallback(() => onClose?.(true), [onClose]);
|
||||
|
||||
return (
|
||||
<div style={modalHeaderStyle}>
|
||||
<span style={modalTitleStyle}>{title}</span>
|
||||
{onClose && (
|
||||
<button onClick={doClose} title="Close">
|
||||
{'\u2715'}
|
||||
</button>
|
||||
)}
|
||||
{onClose && <ModalCloseButton onClose={onClose} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { includes } from 'js/types';
|
||||
import { initGamepad } from 'js/ui/gamepad';
|
||||
import {
|
||||
DISK_FORMATS,
|
||||
DriveNumber,
|
||||
JSONDisk,
|
||||
NIBBLE_FORMATS
|
||||
|
@ -46,24 +45,23 @@ export const loadLocalFile = (
|
|||
fileReader.onload = function () {
|
||||
const result = this.result as ArrayBuffer;
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts.pop()!.toLowerCase();
|
||||
const ext = parts.pop()?.toLowerCase() || '[none]';
|
||||
const name = parts.join('.');
|
||||
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (includes(NIBBLE_FORMATS, ext)) {
|
||||
if (result.byteLength >= 800 * 1024) {
|
||||
reject(`Unable to load ${name}`);
|
||||
} else {
|
||||
if (
|
||||
includes(NIBBLE_FORMATS, ext) &&
|
||||
disk2?.setBinary(number, name, ext, result)
|
||||
) {
|
||||
initGamepad();
|
||||
initGamepad();
|
||||
if (disk2.setBinary(number, name, ext, result)) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(`Unable to load ${name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reject(`Extension ${ext} not recognized.`);
|
||||
}
|
||||
resolve(true);
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
});
|
||||
|
@ -81,15 +79,14 @@ export const loadLocalFile = (
|
|||
*/
|
||||
export const loadJSON = async (disk2: DiskII, number: DriveNumber, url: string) => {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data: JSONDisk = await response.json();
|
||||
if (includes(NIBBLE_FORMATS, data.type)) {
|
||||
disk2.setDisk(number, data);
|
||||
}
|
||||
initGamepad(data.gamepad);
|
||||
} else {
|
||||
throw new Error('Error loading: ' + response.statusText);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error loading: ${response.statusText}`);
|
||||
}
|
||||
const data: JSONDisk = await response.json();
|
||||
if (includes(NIBBLE_FORMATS, data.type)) {
|
||||
disk2.setDisk(number, data);
|
||||
}
|
||||
initGamepad(data.gamepad);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -109,40 +106,37 @@ export const loadHttpFile = async (
|
|||
) => {
|
||||
if (url.endsWith('.json')) {
|
||||
return loadJSON(disk2, number, url);
|
||||
} else {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const reader = response.body!.getReader();
|
||||
let received = 0;
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
let result = await reader.read();
|
||||
while (!result.done) {
|
||||
chunks.push(result.value);
|
||||
received += result.value.length;
|
||||
result = await reader.read();
|
||||
}
|
||||
|
||||
const data = new Uint8Array(received);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
data.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
const urlParts = url!.split('/');
|
||||
const file = urlParts.pop()!;
|
||||
const fileParts = file.split('.');
|
||||
const ext = fileParts.pop()!.toLowerCase();
|
||||
const name = decodeURIComponent(fileParts.join('.'));
|
||||
if (includes(NIBBLE_FORMATS, ext)) {
|
||||
disk2.setBinary(number, name, ext, data);
|
||||
initGamepad();
|
||||
} else {
|
||||
throw new Error(`Extension ${ext} not recognized.`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Error loading: ' + response.statusText);
|
||||
}
|
||||
}
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error loading: ${response.statusText}`);
|
||||
}
|
||||
const reader = response.body!.getReader();
|
||||
let received = 0;
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
let result = await reader.read();
|
||||
while (!result.done) {
|
||||
chunks.push(result.value);
|
||||
received += result.value.length;
|
||||
result = await reader.read();
|
||||
}
|
||||
|
||||
const data = new Uint8Array(received);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
data.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
const urlParts = url.split('/');
|
||||
const file = urlParts.pop()!;
|
||||
const fileParts = file.split('.');
|
||||
const ext = fileParts.pop()?.toLowerCase() || '[none]';
|
||||
const name = decodeURIComponent(fileParts.join('.'));
|
||||
if (!includes(NIBBLE_FORMATS, ext)) {
|
||||
throw new Error(`Extension ${ext} not recognized.`);
|
||||
}
|
||||
disk2.setBinary(number, name, ext, data);
|
||||
initGamepad();
|
||||
};
|
||||
|
|
|
@ -221,6 +221,7 @@ export type KeyFunction = (key: KeyboardEvent) => void;
|
|||
* @returns ASCII character
|
||||
*/
|
||||
export const mapKeyEvent = (evt: KeyboardEvent, caps: boolean) => {
|
||||
// TODO(whscullin): Find replacement for deprecated keycode
|
||||
const code = evt.keyCode;
|
||||
let key: byte = 0xff;
|
||||
|
||||
|
@ -233,7 +234,7 @@ export const mapKeyEvent = (evt: KeyboardEvent, caps: boolean) => {
|
|||
key -= 0x20;
|
||||
}
|
||||
} else {
|
||||
debug('Unhandled key = ' + toHex(code));
|
||||
debug(`Unhandled key = ${toHex(code)}`);
|
||||
}
|
||||
|
||||
return key;
|
||||
|
|
Loading…
Reference in New Issue
Block a user