All files / src/web drives.js

91.66% Statements 66/72
87.5% Branches 42/48
100% Functions 16/16
89.65% Lines 52/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142              1161x 21x                 141x 141x 141x 141x 141x 141x   141x 282x 282x 282x           7x 7x 7x                 5x 5x 2x 2x                   2x 2x             2x 2x           49x         354x 18x               10x 10x 9x 9x 8x 8x 8x 8x               6x 6x 1x       67x 65x 65x 65x 65x 65x   65x       6x       65x 62x 4x 3x 3x               3x 3x            
import { toast } from "./toast.js";
import { DiscLayout, toSsdOrDsd } from "../disc.js";
import { toHfe } from "../disc-hfe.js";
import { downloadDriveData } from "./dom-utils.js";
import { DriveTracks } from "../url-params.js";
 
/** The 40/80 switch's label for a drive stepping `tracksPerStep` physical tracks per logical one. */
export const tracksLabel = (tracksPerStep) => (tracksPerStep === 2 ? "40" : "80");
export const tracksPerStepOf = (label) => (label === "40" ? 2 : 1);
 
/**
 * The disc drives as the page sees them: putting a disc in and taking it out,
 * each drive's 40/80 track switch, and downloading what a drive holds.
 * Raises "tracks-changed" when a switch moves.
 */
export class Drives extends EventTarget {
    constructor({ fdc, driveTracks, confirm, urlState }) {
        super();
        this.fdc = fdc;
        this.driveTracks = driveTracks;
        this.confirm = confirm;
        this.urlState = urlState;
        this.saidWritesAreNotKept = false;
 
        for (const driveIndex of [0, 1]) {
            const drive = fdc?.drives[driveIndex];
            const fixed = drive ? this.tracksPerStepForDrive(driveIndex) : undefined;
            if (fixed !== undefined) drive.tracksPerStep = fixed;
        }
    }
 
    /** @returns {import("../disc.js").Disc|null} the disc in the drive, saying so when there is nothing to download */
    discToDownload(driveIndex) {
        const disc = this.fdc?.drives[driveIndex].disc;
        if (!disc) toast(`There is no disc in drive ${driveIndex} to download.`, { title: "Disc" });
        return disc ?? null;
    }
 
    /**
     * A drive's disc as a sector image, asking first when a track would not fit one.
     *
     * @returns {Promise<Uint8Array|null>} the image, or null when there is none to give
     */
    async sectorImage(driveIndex) {
        const disc = this.discToDownload(driveIndex);
        if (!disc) return null;
        try {
            return toSsdOrDsd(disc);
        } catch (e) {
            if (await this.confirm(`${e.message} Save anyway, losing what will not fit?`, "Save anyway", "Cancel"))
                return toSsdOrDsd(disc, { force: true });
            return null;
        }
    }
 
    /** Downloads a drive's disc as a sector image, asking first if a flux-only track would be lost. */
    async downloadSsdOrDsd(driveIndex) {
        const image = await this.sectorImage(driveIndex);
        Eif (!image) return;
        const disc = this.fdc.drives[driveIndex].disc;
        downloadDriveData(image, disc.name, disc.isDoubleSided ? ".dsd" : ".ssd");
    }
 
    /** Downloads a drive's disc as HFE, the format that keeps every flux transition. */
    downloadHfe(driveIndex) {
        const disc = this.discToDownload(driveIndex);
        Eif (!disc) return;
        downloadDriveData(toHfe(disc), disc.name, ".hfe");
    }
 
    /** @returns {string} the DiscLayout to load an image for this drive with */
    layoutForDrive(driveIndex) {
        return this.driveTracks[driveIndex] === DriveTracks.eighty ? DiscLayout.contiguous : DiscLayout.auto;
    }
 
    /** @returns {Number|undefined} the tracksPerStep the user fixed this drive at, if they fixed one */
    tracksPerStepForDrive(driveIndex) {
        if (this.driveTracks[driveIndex] === DriveTracks.auto) return undefined;
        return tracksPerStepOf(this.driveTracks[driveIndex]);
    }
 
    /**
     * Throws a drive's 40/80 switch: the disc in it now is read at that pitch, and so is
     * whatever is put in next, until the switch is thrown again.
     */
    setTracksPerStep(driveIndex, tracksPerStep) {
        const drive = this.fdc?.drives[driveIndex];
        if (!drive) return;
        const setting = tracksLabel(tracksPerStep);
        if (drive.tracksPerStep === tracksPerStep && this.driveTracks[driveIndex] === setting) return;
        drive.tracksPerStep = tracksPerStep;
        this.driveTracks[driveIndex] = setting;
        this.urlState.set({ [`drive${driveIndex}Tracks`]: setting });
        this.dispatchEvent(new CustomEvent("tracks-changed", { detail: { driveIndex } }));
    }
 
    /**
     * A restored state has set the drive's pitch behind the switch's back. A switch the user
     * pinned follows it, so the URL and the drive agree; one on auto has nothing to say.
     */
    followRestoredPitch(driveIndex) {
        const drive = this.fdc?.drives[driveIndex];
        if (drive && this.tracksPerStepForDrive(driveIndex) !== undefined)
            this.setTracksPerStep(driveIndex, drive.tracksPerStep);
    }
 
    putDiscIn(driveIndex, loadedDisc) {
        if (!this.fdc) throw new Error("This machine has no disc drives");
        const drive = this.fdc.drives[driveIndex];
        const fixed = this.tracksPerStepForDrive(driveIndex);
        const was = drive.tracksPerStep;
        this.fdc.loadDisc(driveIndex, loadedDisc, fixed);
        this.noteUnsavedWrites(loadedDisc);
        // A switch the user fixed does not move, so anything it does is not news.
        if (fixed === undefined && drive.tracksPerStep !== was) this.noteDriveTracks(driveIndex, loadedDisc.name);
    }
 
    eject(driveIndex) {
        this.fdc.loadDisc(driveIndex, undefined, this.tracksPerStepForDrive(driveIndex));
    }
 
    noteUnsavedWrites(loadedDisc) {
        if (loadedDisc.savesChanges || this.saidWritesAreNotKept) return;
        loadedDisc.notifyOnFirstTrackWrite(() => {
            if (this.saidWritesAreNotKept) return;
            this.saidWritesAreNotKept = true;
            toast(`Changes to ${loadedDisc.name} are not saved. Use the drive's Save button to keep a copy.`, {
                title: "Disc",
                quietKey: "quietDiscNotSaved",
            });
        });
    }
 
    noteDriveTracks(driveIndex, discName) {
        const tracks = tracksLabel(this.fdc.drives[driveIndex].tracksPerStep);
        toast(`Drive ${driveIndex} switched to ${tracks} track for ${discName}.`, {
            title: "Disc drive",
            quietKey: "quietDriveTracks",
        });
    }
}