mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-03 08:32:52 +00:00
chord importing
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
{
|
||||
" ": "␣"
|
||||
"SPA": "␣"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<h1>dot i/o</h1>
|
||||
<a href="/" class="title">dot i/o</a>
|
||||
|
||||
<div class="steps">
|
||||
<a href={base} title="CPM - characters per minute" class="icon train cpm">music_note</a>
|
||||
@@ -45,9 +45,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.title {
|
||||
margin-block: 0;
|
||||
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {LineBreakTransformer} from "$lib/serial/webserial/util/line-break-transformer.js"
|
||||
import {LineBreakTransformer} from "$lib/serial/line-break-transformer.js"
|
||||
import {serialLog} from "$lib/serial/connection.js"
|
||||
import {ACTION_MAP} from "$lib/serial/webserial/constants/action-map.js"
|
||||
|
||||
export class CharaDevice {
|
||||
/** @type {Promise<SerialPort>} */
|
||||
@@ -127,7 +128,43 @@ export class CharaDevice {
|
||||
async send(...command) {
|
||||
return this.runWith(async (send, read) => {
|
||||
await send(...command)
|
||||
return read()
|
||||
const commandString = command.join(" ").replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
|
||||
return read().then(it => it.replace(new RegExp(`^${commandString} `), ""))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async getChordCount() {
|
||||
return Number.parseInt(await this.send("CML C0"))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index {number}
|
||||
* @returns {Promise<{actions: number[]; phrase: string, unk: number}>}
|
||||
*/
|
||||
async getChord(index) {
|
||||
const chord = await this.send(`CML C1 ${index}`)
|
||||
const [keys, rawPhrase, b] = chord.split(" ")
|
||||
let phrase = []
|
||||
for (let i = 0; i < rawPhrase.length; i += 2) {
|
||||
phrase.push(Number.parseInt(rawPhrase.substring(i, i + 2), 16))
|
||||
}
|
||||
let bigKeys = BigInt(`0x${keys}`)
|
||||
let actions = []
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const action = Number(bigKeys & BigInt(0b1111111111))
|
||||
if (action !== 0) {
|
||||
actions.push(ACTION_MAP[action])
|
||||
}
|
||||
bigKeys >>= BigInt(10)
|
||||
}
|
||||
|
||||
return {
|
||||
actions,
|
||||
phrase: String.fromCodePoint(...phrase),
|
||||
unk: Number(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import {LineBreakTransformer} from "$lib/serial/webserial/util/line-break-transformer.js"
|
||||
import {CONFIG_ID} from "$lib/serial/webserial/constants/config-id.js"
|
||||
let _chordMaps = []
|
||||
|
||||
let serialPort = null
|
||||
let portReader = null
|
||||
let lineReader = null
|
||||
let lineReaderDone = null
|
||||
let abortController1 = new AbortController()
|
||||
let abortController2 = new AbortController()
|
||||
|
||||
async function disconnectSerialConnection() {
|
||||
console.log("disconnectSerialConnection()")
|
||||
if (serialPort) {
|
||||
console.log("closing serial port")
|
||||
lineReader.releaseLock()
|
||||
|
||||
console.log(serialPort.readable)
|
||||
await abortController1.abort()
|
||||
await lineReaderDone.catch(() => {
|
||||
/* Ingore the error */
|
||||
})
|
||||
await serialPort.close()
|
||||
|
||||
console.log("serial port is closed")
|
||||
document.getElementById("statusDiv").innerHTML = "status: closed serial port"
|
||||
} else {
|
||||
console.log("there is no serial connection open to close")
|
||||
}
|
||||
}
|
||||
|
||||
//TODO not sure this actually works
|
||||
async function cancelReader() {
|
||||
if (serialPort) {
|
||||
if (lineReader) {
|
||||
// if(lineReader.locked){
|
||||
await lineReader.cancel().then(() => {
|
||||
console.log("cleared line reader")
|
||||
})
|
||||
// await serialPort.readable.releaseLock();
|
||||
console.log(abortController1)
|
||||
await abortController1.abort()
|
||||
console.log(serialPort.readable)
|
||||
await lineReaderDone.catch(() => {
|
||||
/* Ingore the error */
|
||||
}) //this frees up the serialPort.readable after the abortControl1.abort() signal
|
||||
// await serialPort.readable.cancel();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resetReader() {
|
||||
console.log("resetting lineReader")
|
||||
if (serialPort) {
|
||||
if (lineReader) {
|
||||
if (lineReader.locked) {
|
||||
await lineReader.releaseLock()
|
||||
}
|
||||
await lineReader.cancel().then(() => {
|
||||
console.log("cleared line reader")
|
||||
})
|
||||
await lineReaderDone.catch(() => {
|
||||
/* Ingore the error */
|
||||
})
|
||||
}
|
||||
await setupLineReader()
|
||||
}
|
||||
console.log("reset lineReader")
|
||||
}
|
||||
|
||||
async function getCount() {
|
||||
await sendCommandString("SELECT BASE")
|
||||
await readGetChordmapCount()
|
||||
document.getElementById("countDiv").innerHTML = "count: " + _chordmapCountOnDevice
|
||||
}
|
||||
|
||||
async function getGetAll1() {
|
||||
await selectBase() //select BASE
|
||||
await sendCommandString("GETALL")
|
||||
await readGetAllChordmaps()
|
||||
}
|
||||
|
||||
async function getGetAll2() {
|
||||
await selectBase() //select BASE
|
||||
await sendCommandString("GETSOME 0 " + _chordmapCountOnDevice)
|
||||
await readGetSomeChordmaps(_chordmapCountOnDevice)
|
||||
}
|
||||
|
||||
async function getGetAll() {
|
||||
await selectBase() //select BASE
|
||||
for (let i = 0; i < _chordmapCountOnDevice; i++) {
|
||||
await sendCommandString("GETSOME " + (i + 0).toString() + " " + (i + 1).toString())
|
||||
await readGetOneChordmap()
|
||||
}
|
||||
}
|
||||
|
||||
let _chordmapId = "DEFAULT"
|
||||
let _chordmapCountOnDevice = 50 //TODO set this to zero by default
|
||||
let _firmwareVersion = "0"
|
||||
|
||||
async function readGetChordmapCount() {
|
||||
const {value, done} = await lineReader.read()
|
||||
if (value) {
|
||||
_chordmapCountOnDevice = parseInt(value)
|
||||
console.log(_chordmapCountOnDevice)
|
||||
}
|
||||
}
|
||||
|
||||
async function readGetAll() {
|
||||
readGetSomeChordmaps(_chordmapCountOnDevice)
|
||||
}
|
||||
|
||||
function commitAll() {
|
||||
console.log("commitAll()")
|
||||
const dataTable = document.getElementById("dataTable")
|
||||
//iterate through table from bottom to top to see if there's a commit enabled
|
||||
//TODO check if we need to skip the header row
|
||||
for (let i = dataTable.rows.length - 1; i >= 1; i--) {
|
||||
//iterate through rows
|
||||
let row = dataTable.rows[i]
|
||||
// console.log(row);
|
||||
// console.log(row.cells);
|
||||
// console.log(row.cells[0]);
|
||||
// console.log(row.cells[0].innerHTML);
|
||||
let virtualId = parseInt(row.cells[0].innerHTML)
|
||||
console.log("table row " + i + " has virtualId of " + virtualId)
|
||||
// document.getElementById(virtualId.toString()+"-commit")
|
||||
setTimeout(pressCommitButton, i * 100, virtualId)
|
||||
//rows would be accessed using the "row" variable assigned in the for loop
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export async function bootLoader() {
|
||||
//Sends the bootloader command to the charachorder via the serial API
|
||||
await sendCommandString("BOOTLOADER")
|
||||
await readGetNone()
|
||||
}
|
||||
|
||||
export async function reboot() {
|
||||
//Sends the restart command to the charachorder via the serial API
|
||||
await sendCommandString("RESTART")
|
||||
await readGetNone()
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
* `38,655,229,952`
|
||||
* `0x00000000900080000`
|
||||
*/
|
||||
export const _actionMap = [
|
||||
export const ACTION_MAP = [
|
||||
"NUL", //0x00 0
|
||||
"CCFunc", //0x01
|
||||
"STX", //0x02 2
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import {readGetOneAndToss} from "$lib/serial/webserial/noop.js"
|
||||
|
||||
/**
|
||||
* Unused?
|
||||
* @param lineReader {ReadableStreamDefaultReader<string>}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function readGetHexChord(lineReader) {
|
||||
let hexChordString = ""
|
||||
await readGetOneAndToss(lineReader) //this is added for the latest firmware with customers, where decimal version
|
||||
|
||||
const {value, done} = await lineReader.read()
|
||||
if (done) {
|
||||
console.log("reader is done")
|
||||
} else {
|
||||
console.log(["value", value])
|
||||
|
||||
if (value) {
|
||||
let arrValue = [...value]
|
||||
const strValue = String(arrValue.join(""))
|
||||
console.log(strValue)
|
||||
hexChordString = strValue.substr(0, 16)
|
||||
await readGetOneAndToss(lineReader) // the "processing chord:" decimal output
|
||||
}
|
||||
}
|
||||
return hexChordString
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* @param lineReader {ReadableStreamDefaultReader<string>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function readGetOneAndToss(lineReader) {}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* JavaScript handles up to 53-bit bs it uses float64 to operate with integers
|
||||
*/
|
||||
function bitwiseAndLarge(val1, val2) {
|
||||
let shift = 0,
|
||||
result = 0
|
||||
const mask = ~(~0 << 30) // Gives us a bit mask like 01111..1 (30 ones)
|
||||
const divisor = 1 << 30 // To work with the bit mask, we need to clear bits at a time
|
||||
while (val1 !== 0 && val2 !== 0) {
|
||||
let rs = mask & val1 & (mask & val2)
|
||||
val1 = Math.floor(val1 / divisor) // val1 >>> 30
|
||||
val2 = Math.floor(val2 / divisor) // val2 >>> 30
|
||||
for (let i = shift++; i--; ) {
|
||||
rs *= divisor // rs << 30
|
||||
}
|
||||
result += rs
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import {_actionMap} from "$lib/serial/webserial/constants/action-map.js"
|
||||
import {_keyMap} from "$lib/serial/webserial/constants/key-map.js"
|
||||
import {_keyMapDefaults} from "$lib/serial/webserial/constants/key-map-defaults.js"
|
||||
|
||||
/**
|
||||
* @param hexString {string}
|
||||
* @param chordMapId {keyof typeof import('$lib/serial/webserial/constants/key-map-defaults.js')._keyMapDefaults}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function convertHexadecimalChordToHumanString(hexString, chordMapId) {
|
||||
let humanString = ""
|
||||
console.log(hexString)
|
||||
if (hexString.length <= 0) {
|
||||
hexString = "00"
|
||||
}
|
||||
let bigNum = BigInt("0x" + hexString)
|
||||
|
||||
if (chordMapId === "CHARACHORDER") {
|
||||
// CharaChorder original uses different key map structure
|
||||
let decString = String(bigNum).split("") //no left zeros; that's ok
|
||||
console.log(decString)
|
||||
for (let i = 0; i < decString.length; i++) {
|
||||
if (decString[i] !== "0") {
|
||||
if (humanString.length > 0) {
|
||||
humanString += " + "
|
||||
}
|
||||
console.log({
|
||||
"i": i,
|
||||
"decString[i]": decString[i],
|
||||
"decString.length": decString.length,
|
||||
"decString": decString,
|
||||
"10exp": decString.length - i - 1,
|
||||
"decChordComp": decString[i] * 10 ** (decString.length - i - 1),
|
||||
// 'decChordCompBigInt':BigInt(decString[i])*BigInt((BigInt(10)**(decString.length-i-1))),
|
||||
"noteId": chord_to_noteId(decString[i] * 10 ** (decString.length - i - 1)),
|
||||
})
|
||||
let noteId
|
||||
let actionId
|
||||
if (decString[i] % 2 === 1) {
|
||||
//if it is odd, then it is simple
|
||||
noteId = chord_to_noteId(decString[i] * 10 ** (decString.length - i - 1))
|
||||
actionId = _keyMapDefaults["CHARACHORDER"][noteId]
|
||||
if (actionId === 0) {
|
||||
actionId = 0x0200 + noteId
|
||||
}
|
||||
humanString += _actionMap[actionId]
|
||||
} else {
|
||||
//value is even, odd plus a 1
|
||||
noteId = chord_to_noteId((decString[i] - 1) * 10 ** (decString.length - i - 1))
|
||||
actionId = _keyMapDefaults["CHARACHORDER"][noteId]
|
||||
if (actionId === 0) {
|
||||
actionId = 0x0200 + noteId
|
||||
}
|
||||
humanString += _actionMap[actionId]
|
||||
|
||||
humanString += " + "
|
||||
|
||||
noteId = chord_to_noteId(10 ** (decString.length - i - 1))
|
||||
actionId = _keyMapDefaults["CHARACHORDER"][noteId]
|
||||
if (actionId === 0) {
|
||||
actionId = 0x0200 + noteId
|
||||
}
|
||||
humanString += _actionMap[actionId]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let binString = bigNum.toString(2) //no left zeros; that's ok
|
||||
console.log(binString)
|
||||
for (let i = 0; i < binString.length; i++) {
|
||||
if (binString[i] === "1") {
|
||||
if (humanString.length > 0) {
|
||||
humanString += " + "
|
||||
}
|
||||
humanString += _keyMap[64 - binString.length + i]
|
||||
//console.log(i);
|
||||
//humanString+=_keyMap[(64-binString.length+i)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(humanString)
|
||||
return humanString
|
||||
}
|
||||
|
||||
/**
|
||||
* @param humanString {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function convertHumanStringToHexadecimalPhrase(humanString) {
|
||||
let hexString = ""
|
||||
for (let i = 0; i < humanString.length; i++) {
|
||||
let hex = Number(humanString.charCodeAt(i)).toString(16)
|
||||
hexString += hex
|
||||
}
|
||||
hexString = hexString.toUpperCase()
|
||||
console.log(hexString)
|
||||
return hexString
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hexString {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertHexadecimalPhraseToAsciiString(hexString) {
|
||||
let asciiString = ""
|
||||
console.log("convertHexadecimalPhraseToAsciiString()")
|
||||
|
||||
//assume 2x size
|
||||
//get every 2 characters
|
||||
//TODO covert to byte array and account for non-ascii inputs like mouse moves
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
asciiString += String.fromCharCode(parseInt(hexString.substr(i, 2), 16))
|
||||
//console.log("0x"+hexString.substr(i, 2));
|
||||
//asciiString += String.fromCharCode("0x"+hexString.substr(i, 2));
|
||||
}
|
||||
console.log(asciiString)
|
||||
return asciiString
|
||||
}
|
||||
|
||||
function noteId_to_chord(note) {
|
||||
return BigInt(2 * ((note - 1) % 5) + 1) * BigInt(10) ** BigInt(Math.floor((note - 1) / 5))
|
||||
}
|
||||
|
||||
function chord_to_noteId(chord) {
|
||||
const part1 = 5 * Math.floor(Math.log10(chord))
|
||||
const part2 = Math.floor(chord / 10 ** Math.floor(Math.log10(chord)) + 1) / 2
|
||||
const part3 = Math.log10(chord)
|
||||
|
||||
const full = Math.floor(
|
||||
5 * Math.floor(Math.log10(chord)) + Math.floor(chord / 10 ** Math.floor(Math.log10(chord)) + 1) / 2,
|
||||
)
|
||||
console.log([chord, part1, part2, part3, full])
|
||||
return full
|
||||
}
|
||||
|
||||
/**
|
||||
* @param humanString {string}
|
||||
* @param chordMapId {keyof typeof import('$lib/serial/webserial/constants/key-map-defaults.js')._keyMapDefaults}
|
||||
* @returns {Promise<bigint>}
|
||||
*/
|
||||
export async function convertHumanStringToBigNum(humanString, chordMapId) {
|
||||
console.log("convertHumanStringToBigNum")
|
||||
let bigNum = BigInt(0)
|
||||
//parse the pieces with _+_
|
||||
let humanStringParts = humanString.split(" + ") //assumes plus isn't being used; bc default is = for the +/= key
|
||||
console.log(humanStringParts)
|
||||
for (const part of humanStringParts) {
|
||||
let actionId = _actionMap.indexOf(part)
|
||||
console.log(actionId)
|
||||
if (chordMapId === "CHARACHORDER") {
|
||||
//charachorder original uses different key map structure
|
||||
let keyId
|
||||
if (actionId < 0x0200) {
|
||||
keyId = _keyMapDefaults["CHARACHORDER"].indexOf(actionId)
|
||||
} else {
|
||||
keyId = actionId - 0x0200 //using the physical key position
|
||||
}
|
||||
|
||||
console.log(keyId)
|
||||
bigNum = BigInt(noteId_to_chord(keyId))
|
||||
// bigNum+= BigInt(noteId_to_chord(keyId));
|
||||
console.log(bigNum)
|
||||
} else {
|
||||
//use other keymap
|
||||
}
|
||||
}
|
||||
console.log(bigNum)
|
||||
return bigNum
|
||||
}
|
||||
|
||||
/**
|
||||
* @param humanString {string}
|
||||
* @param chordMapId {keyof typeof import('$lib/serial/webserial/constants/key-map-defaults.js')._keyMapDefaults}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function convertHumanStringToHexadecimalChord(humanString, chordMapId) {
|
||||
let bigNum = BigInt(0)
|
||||
//parse the pieces with _+_
|
||||
let humanStringParts = humanString.split(" + ") //assumes plus isn't being used; bc default is = for the +/= key
|
||||
console.log(humanStringParts)
|
||||
humanStringParts.forEach(async part => {
|
||||
let actionId = _actionMap.indexOf(part)
|
||||
console.log(actionId)
|
||||
if (chordMapId === "CHARACHORDER") {
|
||||
//charachorder original uses different key map structure
|
||||
let keyId
|
||||
if (actionId < 0x0200) {
|
||||
keyId = _keyMapDefaults["CHARACHORDER"].indexOf(actionId)
|
||||
} else {
|
||||
keyId = actionId - 0x0200 //using the physical key position
|
||||
}
|
||||
|
||||
console.log(keyId)
|
||||
bigNum += BigInt(noteId_to_chord(keyId))
|
||||
console.log(bigNum)
|
||||
} else {
|
||||
//use other keymap
|
||||
}
|
||||
})
|
||||
console.log(bigNum)
|
||||
|
||||
let hexString = bigNum.toString(16).toUpperCase()
|
||||
hexString = "0".repeat(16 - hexString.length) + hexString //add leading zeros up to 16 characters
|
||||
console.log(hexString)
|
||||
|
||||
return hexString
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import {readGetOneAndToss} from "$lib/serial/webserial/noop.js"
|
||||
|
||||
export let FIRMWARE_VERSION = "0"
|
||||
|
||||
/**
|
||||
* @param lineReader {ReadableStreamDefaultReader<string>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function readVersion(lineReader) {
|
||||
await readGetOneAndToss(lineReader) //electronics board version
|
||||
const {value, done} = await lineReader.read()
|
||||
if (value) {
|
||||
FIRMWARE_VERSION = value
|
||||
console.log("Firmware Version:", FIRMWARE_VERSION)
|
||||
}
|
||||
await readGetOneAndToss(lineReader) //serial api version
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export const prerender = true
|
||||
export const trailingSlash = "always"
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
<script>
|
||||
import {serialPort} from "$lib/serial/connection.js"
|
||||
import keySymbols from "$lib/assets/key-symbols.json"
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>dot i/o</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>dot i/o V2</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
|
||||
<h2>Chords</h2>
|
||||
{#if $serialPort}
|
||||
{#await $serialPort.getChordCount() then chordCount}
|
||||
<p>You have {chordCount} chords</p>
|
||||
<table>
|
||||
{#each Array.from({length: chordCount}) as _, i}
|
||||
{#await $serialPort.getChord(i) then { phrase, actions }}
|
||||
<tr>
|
||||
<th>{phrase}</th>
|
||||
<td>
|
||||
{#each actions as action}
|
||||
<i>{keySymbols[action] || action}</i>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/await}
|
||||
{/each}
|
||||
</table>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
table i {
|
||||
display: block;
|
||||
|
||||
aspect-ratio: 1;
|
||||
padding-block: 4px;
|
||||
padding-inline: 8px;
|
||||
|
||||
font-style: normal;
|
||||
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,17 +39,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Chords</h2>
|
||||
<div class="icon bg">piano</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>three</th>
|
||||
<td><i>␣</i><i>3</i></td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Serial Terminal</h2>
|
||||
<div class="icon bg">terminal</div>
|
||||
@@ -130,24 +119,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
table td {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
table i {
|
||||
display: block;
|
||||
|
||||
aspect-ratio: 1;
|
||||
padding-block: 4px;
|
||||
padding-inline: 8px;
|
||||
|
||||
font-style: normal;
|
||||
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.device-grid {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
0
static/.nojekyll
Normal file
0
static/.nojekyll
Normal file
Reference in New Issue
Block a user