feat: new blocking progress bar, fixes #18

feat: change cloud icon to history, fixes #15
fix: action search items overlap, fixes #16
feat: show tooltips immediately
This commit is contained in:
2023-11-14 20:19:01 +01:00
parent e19a57efac
commit ebf7d73d20
27 changed files with 790 additions and 268 deletions

View File

@@ -72,6 +72,8 @@ const config: IconsConfig = {
"navigate_next", "navigate_next",
"print", "print",
"restore_from_trash", "restore_from_trash",
"history",
"history_toggle_off",
], ],
codePoints: { codePoints: {
speed: "e9e4", speed: "e9e4",

View File

@@ -44,9 +44,6 @@
"@sveltejs/vite-plugin-svelte": "^2.4.5", "@sveltejs/vite-plugin-svelte": "^2.4.5",
"@tauri-apps/api": "^1.4.0", "@tauri-apps/api": "^1.4.0",
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
"@tiptap/core": "^2.1.12",
"@tiptap/pm": "^2.1.12",
"@tiptap/starter-kit": "^2.1.12",
"@theaninova/prettier-config": "^1.0.0", "@theaninova/prettier-config": "^1.0.0",
"@types/dom-view-transitions": "^1.0.1", "@types/dom-view-transitions": "^1.0.1",
"@types/flexsearch": "^0.7.3", "@types/flexsearch": "^0.7.3",

View File

@@ -9,6 +9,12 @@ const de = {
APPLY: "Anwenden", APPLY: "Anwenden",
SAVE: "Änderungen auf das Gerät schreiben", SAVE: "Änderungen auf das Gerät schreiben",
}, },
sync: {
TITLE_READ: "Neueste Änderungen werden abgerufen",
TITLE_WRITE: "Änderungen werden gebrannt",
DISCLAIMER_WRITE:
"Das Brennen von Änderungen ist nur für Layouts und Einstellungen erforderlich wenn diese Neustarts überdauern sollen. Bei Akkorden passiert das brennen automatisch beim anwenden.",
},
backup: { backup: {
TITLE: "Sicherungskopie", TITLE: "Sicherungskopie",
DISCLAIMER: DISCLAIMER:
@@ -28,9 +34,13 @@ const de = {
}, },
}, },
share: { share: {
TITLE: "Teilen",
URL_COPIED: "Teilbare URL kopiert!", URL_COPIED: "Teilbare URL kopiert!",
EXTRA_DOWNLOAD: "Als Datei herunterladen", EXTRA_DOWNLOAD: "Als Datei herunterladen",
}, },
print: {
TITLE: "Drucken",
},
profile: { profile: {
TITLE: "Profil", TITLE: "Profil",
LANGUAGE: "Sprache", LANGUAGE: "Sprache",
@@ -67,12 +77,21 @@ const de = {
DOWNLOAD_APP: "Desktop-app herunterladen", DOWNLOAD_APP: "Desktop-app herunterladen",
}, },
changes: { changes: {
TITLE: "Änderungen anwenden", TITLE: "Änderungen importieren",
CHORD_ADD: "{0} Akkord{{|e}} hinzugefügt", ALL_CHANGES: "Alle Änderungen",
CHORD_EDIT: "{0} Akkord{{|e}} bearbeitet", layout: {
CHORD_DELETE: "{0} Akkord{{|e}} entfernt", TITLE: "{0} veränderte Belegung{{:|en}}",
SETTING_CHANGE: "{0} Einstellung{{|en}} geändert", LAYER: "{changes} Belegung{{changes:|en}} in Ebene {layer} ändern",
LAYOUT_CHANGE: "{0} Layout-belegung{{|en}} geändert", },
settings: {
TITLE: "{0} Einstellung{{|en}} anpassen",
},
chords: {
TITLE: "{0} Akkorde",
NEW_CHORDS: "{0} neue Akkord{{|e}} hinzufügen",
CHANGED_CHORDS: "{0} Akkord{{|e}} ersetzen",
DELETED_CHORDS: "{0} Akkord{{|e}} zum löschen markieren",
},
}, },
configure: { configure: {
chords: { chords: {

View File

@@ -7,7 +7,7 @@ const en = {
UNDO: "Undo (hold <kbd class='icon'>shift</kbd> to undo all changes)", UNDO: "Undo (hold <kbd class='icon'>shift</kbd> to undo all changes)",
REDO: "Redo", REDO: "Redo",
APPLY: "Apply", APPLY: "Apply",
SAVE: "Write changes to your device", SAVE: "Burn changes to your device",
}, },
backup: { backup: {
TITLE: "Local Backup", TITLE: "Local Backup",
@@ -15,6 +15,12 @@ const en = {
DOWNLOAD: "Download Backup", DOWNLOAD: "Download Backup",
RESTORE: "Restore", RESTORE: "Restore",
}, },
sync: {
TITLE_READ: "Reading latest changes",
TITLE_WRITE: "Burning changes to device",
DISCLAIMER_WRITE:
"Burning is only necessary if you want your layout or settings to persist across reboots. Chords always persist automatically on apply.",
},
modal: { modal: {
CLOSE: "Close", CLOSE: "Close",
}, },
@@ -27,9 +33,13 @@ const en = {
}, },
}, },
share: { share: {
TITLE: "Share",
URL_COPIED: "Sharable URL copied!", URL_COPIED: "Sharable URL copied!",
EXTRA_DOWNLOAD: "Download as file", EXTRA_DOWNLOAD: "Download as file",
}, },
print: {
TITLE: "Print",
},
profile: { profile: {
TITLE: "Profile", TITLE: "Profile",
LANGUAGE: "Language", LANGUAGE: "Language",
@@ -65,12 +75,21 @@ const en = {
DOWNLOAD_APP: "Download the desktop app", DOWNLOAD_APP: "Download the desktop app",
}, },
changes: { changes: {
TITLE: "Apply changes", TITLE: "Import changes",
CHORD_ADD: "{0} chord{{|s}} added", ALL_CHANGES: "All changes",
CHORD_EDIT: "{0} chord{{|s}} edited", layout: {
CHORD_DELETE: "{0} chord{{|s}} deleted", TITLE: "{0} layout change{{|s}}",
SETTING_CHANGE: "{0} setting{{|s}} changed", LAYER: "Update {changes} key{{changes:|s}} in layer {layer}",
LAYOUT_CHANGE: "{0} layout key{{|s}} changed", },
settings: {
TITLE: "Update {0} setting{{|s}}",
},
chords: {
TITLE: "{0} chords",
NEW_CHORDS: "Add {0} new chord{{|s}}",
CHANGED_CHORDS: "Replace {0} chord{{|s}}",
DELETED_CHORDS: "Mark {0} chord{{|s}} for deletion",
},
}, },
configure: { configure: {
chords: { chords: {

View File

@@ -15,311 +15,408 @@ actions:
title: Keyboard Error Undefined title: Keyboard Error Undefined
260: 260:
id: "KEY_A" id: "KEY_A"
keyCode: "KeyA"
title: Keyboard a and A (US English) title: Keyboard a and A (US English)
description: Non US English keyboard users may prefer these Raw Scancodes description: Non US English keyboard users may prefer these Raw Scancodes
261: 261:
id: "KEY_B" id: "KEY_B"
keyCode: "KeyB"
title: Keyboard b and B (US English) title: Keyboard b and B (US English)
262: 262:
id: "KEY_C" id: "KEY_C"
keyCode: "KeyC"
title: Keyboard c and C (US English) title: Keyboard c and C (US English)
263: 263:
id: "KEY_D" id: "KEY_D"
keyCode: "KeyD"
title: Keyboard d and D (US English) title: Keyboard d and D (US English)
264: 264:
id: "KEY_E" id: "KEY_E"
keyCode: "KeyE"
title: Keyboard e and E (US English) title: Keyboard e and E (US English)
265: 265:
id: "KEY_F" id: "KEY_F"
keyCode: "KeyF"
title: Keyboard f and F (US English) title: Keyboard f and F (US English)
266: 266:
id: "KEY_G" id: "KEY_G"
keyCode: "KeyG"
title: Keyboard g and G (US English) title: Keyboard g and G (US English)
267: 267:
id: "KEY_H" id: "KEY_H"
keyCode: "KeyH"
title: Keyboard h and H (US English) title: Keyboard h and H (US English)
268: 268:
id: "KEY_I" id: "KEY_I"
keyCode: "KeyI"
title: Keyboard i and I (US English) title: Keyboard i and I (US English)
269: 269:
id: "KEY_J" id: "KEY_J"
keyCode: "KeyJ"
title: Keyboard j and J (US English) title: Keyboard j and J (US English)
270: 270:
id: "KEY_K" id: "KEY_K"
keyCode: "KeyK"
title: Keyboard k and K (US English) title: Keyboard k and K (US English)
271: 271:
id: "KEY_L" id: "KEY_L"
keyCode: "KeyL"
title: Keyboard l and L (US English) title: Keyboard l and L (US English)
272: 272:
id: "KEY_M" id: "KEY_M"
keyCode: "KeyM"
title: Keyboard m and M (US English) title: Keyboard m and M (US English)
273: 273:
id: "KEY_N" id: "KEY_N"
keyCode: "KeyN"
title: Keyboard n and N (US English) title: Keyboard n and N (US English)
274: 274:
id: "KEY_O" id: "KEY_O"
keyCode: "KeyO"
title: Keyboard o and O (US English) title: Keyboard o and O (US English)
275: 275:
id: "KEY_P" id: "KEY_P"
keyCode: "KeyP"
title: Keyboard p and P (US English) title: Keyboard p and P (US English)
276: 276:
id: "KEY_Q" id: "KEY_Q"
keyCode: "KeyQ"
title: Keyboard q and Q (US English) title: Keyboard q and Q (US English)
277: 277:
id: "KEY_R" id: "KEY_R"
keyCode: "KeyR"
title: Keyboard r and R (US English) title: Keyboard r and R (US English)
278: 278:
id: "KEY_S" id: "KEY_S"
keyCode: "KeyS"
title: Keyboard s and S (US English) title: Keyboard s and S (US English)
279: 279:
id: "KEY_T" id: "KEY_T"
keyCode: "KeyT"
title: Keyboard t and T (US English) title: Keyboard t and T (US English)
280: 280:
id: "KEY_U" id: "KEY_U"
keyCode: "KeyU"
title: Keyboard u and U (US English) title: Keyboard u and U (US English)
281: 281:
id: "KEY_V" id: "KEY_V"
keyCode: "KeyV"
title: Keyboard v and V (US English) title: Keyboard v and V (US English)
282: 282:
id: "KEY_W" id: "KEY_W"
keyCode: "KeyW"
title: Keyboard w and W (US English) title: Keyboard w and W (US English)
283: 283:
id: "KEY_X" id: "KEY_X"
keyCode: "KeyX"
title: Keyboard x and X (US English) title: Keyboard x and X (US English)
284: 284:
id: "KEY_Y" id: "KEY_Y"
keyCode: "KeyY"
title: Keyboard y and Y (US English) title: Keyboard y and Y (US English)
285: 285:
id: "KEY_Z" id: "KEY_Z"
keyCode: "KeyZ"
title: Keyboard z and Z (US English) title: Keyboard z and Z (US English)
286: 286:
id: "KEY_1" id: "KEY_1"
keyCode: "Digit1"
title: Keyboard 1 and ! (US English) title: Keyboard 1 and ! (US English)
287: 287:
id: "KEY_2" id: "KEY_2"
keyCode: "Digit2"
title: Keyboard 2 and @ (US English) title: Keyboard 2 and @ (US English)
288: 288:
id: "KEY_3" id: "KEY_3"
keyCode: "Digit3"
title: Keyboard 3 and # (US English) title: Keyboard 3 and # (US English)
289: 289:
id: "KEY_4" id: "KEY_4"
keyCode: "Digit4"
title: Keyboard 4 and $ (US English) title: Keyboard 4 and $ (US English)
290: 290:
id: "KEY_5" id: "KEY_5"
keyCode: "Digit5"
title: Keyboard 5 and % (US English) title: Keyboard 5 and % (US English)
291: 291:
id: "KEY_6" id: "KEY_6"
keyCode: "Digit6"
title: Keyboard 6 and ^ (US English) title: Keyboard 6 and ^ (US English)
292: 292:
id: "KEY_7" id: "KEY_7"
keyCode: "Digit7"
title: Keyboard 7 and & (US English) title: Keyboard 7 and & (US English)
293: 293:
id: "KEY_8" id: "KEY_8"
keyCode: "Digit8"
title: Keyboard 8 and * (US English) title: Keyboard 8 and * (US English)
294: 294:
id: "KEY_9" id: "KEY_9"
keyCode: "Digit9"
title: Keyboard 9 and ( (US English) title: Keyboard 9 and ( (US English)
295: 295:
id: "KEY_0" id: "KEY_0"
keyCode: "Digit0"
title: Keyboard 0 and ) (US English) title: Keyboard 0 and ) (US English)
296: 296:
id: "ENTER" id: "ENTER"
keyCode: "Enter"
title: Keyboard Return (US English) title: Keyboard Return (US English)
icon: keyboard_return icon: keyboard_return
297: 297:
id: "ESC" id: "ESC"
keyCode: "Escape"
title: Keyboard Escape (US English) title: Keyboard Escape (US English)
298: 298:
id: "BKSP" id: "BKSP"
keyCode: "Backspace"
title: Keyboard Backspace (US English) title: Keyboard Backspace (US English)
icon: backspace icon: backspace
299: 299:
id: "TAB" id: "TAB"
keyCode: "Tab"
title: Keyboard Tab (US English) title: Keyboard Tab (US English)
icon: keyboard_tab icon: keyboard_tab
300: 300:
id: "KSC_2C" id: "KSC_2C"
keyCode: "Space"
title: Keyboard Space (US English) title: Keyboard Space (US English)
description: | description: |
The ASCII space is preferred over this raw scancode for the space bar. The ASCII space is preferred over this raw scancode for the space bar.
icon: space_bar icon: space_bar
301: 301:
id: "KSC_2D" id: "KSC_2D"
keyCode: "Minus"
title: Keyboard - and _ (US English) title: Keyboard - and _ (US English)
302: 302:
id: "KSC_2E" id: "KSC_2E"
keyCode: "Equal"
title: Keyboard = and + (US English) title: Keyboard = and + (US English)
303: 303:
id: "KSC_2F" id: "KSC_2F"
keyCode: "BracketLeft"
title: Keyboard [ and { (US English) title: Keyboard [ and { (US English)
304: 304:
id: "KSC_30" id: "KSC_30"
keyCode: "BracketRight"
title: Keyboard ] and } (US English) title: Keyboard ] and } (US English)
305: 305:
id: "KSC_31" id: "KSC_31"
keyCode: "Backslash"
title: Keyboard \ and | (US English) title: Keyboard \ and | (US English)
306: 306:
id: "KSC_32" id: "KSC_32"
# TODO: also backslash?
title: Keyboard Non-US \# and ~ (US English) title: Keyboard Non-US \# and ~ (US English)
307: 307:
id: "KSC_33" id: "KSC_33"
keyCode: "Semicolon"
title: "Keyboard ; and : (US English)" title: "Keyboard ; and : (US English)"
308: 308:
id: "KSC_34" id: "KSC_34"
keyCode: "Quote"
title: Keyboard ' and " (US English) title: Keyboard ' and " (US English)
309: 309:
id: "KSC_35" id: "KSC_35"
keyCode: "Backquote"
title: Keyboard ` and ~ (US English) title: Keyboard ` and ~ (US English)
310: 310:
id: "KSC_36" id: "KSC_36"
keyCode: "Comma"
title: Keyboard , and < (US English) title: Keyboard , and < (US English)
311: 311:
id: "KSC_37" id: "KSC_37"
keyCode: "Period"
title: Keyboard . and > (US English) title: Keyboard . and > (US English)
312: 312:
id: "KSC_38" id: "KSC_38"
keyCode: "Slash"
title: Keyboard / and ? (US English) title: Keyboard / and ? (US English)
313: 313:
id: "CAPSLOCK" id: "CAPSLOCK"
keyCode: "CapsLock"
title: Keyboard Caps Lock title: Keyboard Caps Lock
icon: shift_lock icon: shift_lock
314: 314:
id: "F1" id: "F1"
keyCode: "F1"
title: Keyboard F1 title: Keyboard F1
315: 315:
id: "F2" id: "F2"
keyCode: "F2"
title: Keyboard F2 title: Keyboard F2
316: 316:
id: "F3" id: "F3"
keyCode: "F3"
title: Keyboard F3 title: Keyboard F3
317: 317:
id: "F4" id: "F4"
keyCode: "F4"
title: Keyboard F4 title: Keyboard F4
318: 318:
id: "F5" id: "F5"
keyCode: "F5"
title: Keyboard F5 title: Keyboard F5
319: 319:
id: "F6" id: "F6"
keyCode: "F6"
title: Keyboard F6 title: Keyboard F6
320: 320:
id: "F7" id: "F7"
keyCode: "F7"
title: Keyboard F7 title: Keyboard F7
321: 321:
id: "F8" id: "F8"
keyCode: "F8"
title: Keyboard F8 title: Keyboard F8
322: 322:
id: "F9" id: "F9"
keyCode: "F9"
title: Keyboard F9 title: Keyboard F9
323: 323:
id: "F10" id: "F10"
keyCode: "F10"
title: Keyboard F10 title: Keyboard F10
324: 324:
id: "F11" id: "F11"
keyCode: "F11"
title: Keyboard F11 title: Keyboard F11
325: 325:
id: "F12" id: "F12"
keyCode: "F12"
title: Keyboard F12 title: Keyboard F12
326: 326:
id: "PRTSCN" id: "PRTSCN"
keyCode: "PrintScreen"
title: Keyboard Print Screen title: Keyboard Print Screen
icon: screenshot_monitor icon: screenshot_monitor
327: 327:
id: "SCRLK" id: "SCRLK"
keyCode: "ScrollLock"
title: Keyboard Scroll Lock title: Keyboard Scroll Lock
328: 328:
id: "PAUSE" id: "PAUSE"
keyCode: "Pause"
title: Keyboard Pause title: Keyboard Pause
329: 329:
id: "INSERT" id: "INSERT"
keyCode: "Insert"
title: Keyboard Insert title: Keyboard Insert
icon: insert_text icon: insert_text
330: 330:
id: "HOME" id: "HOME"
keyCode: "Home"
title: Keyboard Home title: Keyboard Home
icon: home icon: home
331: 331:
id: "PGUP" id: "PGUP"
keyCode: "PageUp"
title: Keyboard Page Up title: Keyboard Page Up
icon: move_up icon: move_up
332: 332:
id: "DELETE" id: "DELETE"
keyCode: "Delete"
title: Keyboard Delete Forward title: Keyboard Delete Forward
333: 333:
id: "END" id: "END"
keyCode: "End"
title: Keyboard End title: Keyboard End
334: 334:
id: "PGDN" id: "PGDN"
keyCode: "PageDown"
title: Keyboard Page Down title: Keyboard Page Down
icon: move_down icon: move_down
335: 335:
id: "ARROW_RT" id: "ARROW_RT"
keyCode: "ArrowRight"
title: Keyboard Right Arrow title: Keyboard Right Arrow
icon: keyboard_arrow_right icon: keyboard_arrow_right
336: 336:
id: "ARROW_LF" id: "ARROW_LF"
keyCode: "ArrowLeft"
title: Keyboard Left Arrow title: Keyboard Left Arrow
icon: keyboard_arrow_left icon: keyboard_arrow_left
337: 337:
id: "ARROW_DN" id: "ARROW_DN"
keyCode: "ArrowDown"
title: Keyboard Down Arrow title: Keyboard Down Arrow
icon: keyboard_arrow_down icon: keyboard_arrow_down
338: 338:
id: "ARROW_UP" id: "ARROW_UP"
keyCode: "ArrowUp"
title: Keyboard Up Arrow title: Keyboard Up Arrow
icon: keyboard_arrow_up icon: keyboard_arrow_up
339: 339:
id: "NUMLOCK" id: "NUMLOCK"
keyCode: "NumLock"
title: Keyboard Num Lock and Clear title: Keyboard Num Lock and Clear
340: 340:
id: "KP_SLASH" id: "KP_SLASH"
keyCode: "NumpadDivide"
title: Keypad / title: Keypad /
341: 341:
id: "KP_ASTER" id: "KP_ASTER"
keyCode: "NumpadStar"
title: Keypad * title: Keypad *
342: 342:
id: "KP_MINUS" id: "KP_MINUS"
keyCode: "NumpadSubtract"
title: Keypad - title: Keypad -
343: 343:
id: "KP_PLUS" id: "KP_PLUS"
keyCode: "NumpadAdd"
title: Keypad + title: Keypad +
344: 344:
id: "KP_ENTER" id: "KP_ENTER"
keyCode: "NumpadEnter"
title: Keypad Enter title: Keypad Enter
345: 345:
id: "KP_1" id: "KP_1"
keyCode: "Numpad1"
title: Keypad 1 and End title: Keypad 1 and End
346: 346:
id: "KP_2" id: "KP_2"
keyCode: "Numpad2"
title: Keypad 2 and Down Arrow title: Keypad 2 and Down Arrow
347: 347:
id: "KP_3" id: "KP_3"
keyCode: "Numpad3"
title: Keypad 3 and Page Down title: Keypad 3 and Page Down
348: 348:
id: "KP_4" id: "KP_4"
keyCode: "Numpad4"
title: Keypad 4 and Left Arrow title: Keypad 4 and Left Arrow
349: 349:
id: "KP_5" id: "KP_5"
keyCode: "Numpad5"
title: Keypad 5 title: Keypad 5
350: 350:
id: "KP_6" id: "KP_6"
keyCode: "Numpad6"
title: Keypad 6 and Rigth Arrow title: Keypad 6 and Rigth Arrow
351: 351:
id: "KP_7" id: "KP_7"
keyCode: "Numpad7"
title: Keypad 7 and Home title: Keypad 7 and Home
352: 352:
id: "KP_8" id: "KP_8"
keyCode: "Numpad8"
title: Keypad 8 and Up Arrow title: Keypad 8 and Up Arrow
353: 353:
id: "KP_9" id: "KP_9"
keyCode: "Numpad9"
title: Keypad 9 and Page Up title: Keypad 9 and Page Up
354: 354:
id: "KP_0" id: "KP_0"
keyCode: "Numpad0"
title: Keypad 0 and Insert title: Keypad 0 and Insert
355: 355:
id: "KP_DOT" id: "KP_DOT"
keyCode: "NumpadDecimal"
title: Keypad . and Delete title: Keypad . and Delete
356: 356:
id: "KSC_64" id: "KSC_64"
keyCode: "IntlBackslash"
title: Keyboard Non-US \ and | (US English) title: Keyboard Non-US \ and | (US English)
357: 357:
id: "COMPOSE" id: "COMPOSE"
@@ -327,10 +424,12 @@ actions:
description: Officially supported by Win, Unix, and Boot description: Officially supported by Win, Unix, and Boot
358: 358:
id: "POWER" id: "POWER"
keyCode: "Power"
title: Keyboard Power title: Keyboard Power
description: Only officially supported by Mac and Unix description: Only officially supported by Mac and Unix
359: 359:
id: "KP_EQUAL" id: "KP_EQUAL"
keyCode: "NumpadEqual"
title: Keypad = title: Keypad =
description: Only officially supported by Mac description: Only officially supported by Mac
360: 360:
@@ -787,10 +886,12 @@ actions:
description: Not required to be supported by any OS description: Not required to be supported by any OS
472: 472:
id: "KSC_D8" id: "KSC_D8"
keyCode: "NumpadClear"
title: Keypad Clear title: Keypad Clear
description: Not required to be supported by any OS description: Not required to be supported by any OS
473: 473:
id: "KSC_D9" id: "KSC_D9"
keyCode: "NumpadClearEntry"
title: Keypad Clear Entry title: Keypad Clear Entry
description: Not required to be supported by any OS description: Not required to be supported by any OS
474: 474:
@@ -817,58 +918,74 @@ actions:
description: Not required to be supported by any OS description: Not required to be supported by any OS
480: 480:
id: "KSC_E0" id: "KSC_E0"
keyCode: "ControlLeft"
title: Keyboard Left Control title: Keyboard Left Control
481: 481:
id: "KSC_E1" id: "KSC_E1"
keyCode: "ShiftLeft"
title: Keyboard Left Shift title: Keyboard Left Shift
482: 482:
id: "KSC_E2" id: "KSC_E2"
keyCode: "AltLeft"
title: Keyboard Left Alt title: Keyboard Left Alt
483: 483:
id: "KSC_E3" id: "KSC_E3"
keyCode: "MetaLeft"
title: Keyboard Left GUI title: Keyboard Left GUI
484: 484:
id: "KSC_E4" id: "KSC_E4"
keyCode: "ControlRight"
title: Keyboard Right Control title: Keyboard Right Control
485: 485:
id: "KSC_E5" id: "KSC_E5"
keyCode: "ShiftRight"
title: Keyboard Right Shift title: Keyboard Right Shift
486: 486:
id: "KSC_E6" id: "KSC_E6"
keyCode: "AltRight"
title: Keyboard Right Alt title: Keyboard Right Alt
487: 487:
id: "KSC_E7" id: "KSC_E7"
keyCode: "MetaRight"
title: Keyboard Right GUI title: Keyboard Right GUI
488: 488:
id: "KSC_E8" id: "KSC_E8"
keyCode: "MediaPlayPause"
title: Media Play Pause title: Media Play Pause
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
489: 489:
id: "KSC_E9" id: "KSC_E9"
keyCode: "MediaStop"
title: Media Stop CD title: Media Stop CD
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
490: 490:
id: "KSC_EA" id: "KSC_EA"
keyCode: "MediaTrackPrevious"
title: Media Previous Song title: Media Previous Song
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
491: 491:
id: "KSC_EB" id: "KSC_EB"
keyCode: "MediaTrackNext"
title: Media Next Song title: Media Next Song
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
492: 492:
id: "KSC_EC" id: "KSC_EC"
keyCode: "Eject"
title: Media Eject CD title: Media Eject CD
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
493: 493:
id: "KSC_ED" id: "KSC_ED"
keyCode: "AudioVolumeUp"
title: Media Volume Up title: Media Volume Up
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
494: 494:
id: "KSC_EE" id: "KSC_EE"
keyCode: "AudioVolumeDown"
title: Media Volume Down title: Media Volume Down
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
495: 495:
id: "KSC_EF" id: "KSC_EF"
keyCode: "AudioVolumeMute"
title: Media Mute title: Media Mute
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
496: 496:
@@ -877,18 +994,22 @@ actions:
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
497: 497:
id: "KSC_F1" id: "KSC_F1"
keyCode: "BrowserBack"
title: Media Back title: Media Back
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
498: 498:
id: "KSC_F2" id: "KSC_F2"
keyCode: "BrowserForward"
title: Media Forward title: Media Forward
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
499: 499:
id: "KSC_F3" id: "KSC_F3"
keyCode: "BrowserStop"
title: Media Stop title: Media Stop
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
500: 500:
id: "KSC_F4" id: "KSC_F4"
keyCode: "BrowserSearch"
title: Media Find title: Media Find
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
501: 501:
@@ -905,14 +1026,17 @@ actions:
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
504: 504:
id: "KSC_F8" id: "KSC_F8"
keyCode: "Sleep"
title: Media Sleep title: Media Sleep
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
505: 505:
id: "KSC_F9" id: "KSC_F9"
keyCode: "WakeUp"
title: Media Coffee title: Media Coffee
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
506: 506:
id: "KSC_FA" id: "KSC_FA"
keyCode: "BrowserRefresh"
title: Media Refresh title: Media Refresh
description: Not required to be supported by any OS. Possibly deprecated. description: Not required to be supported by any OS. Possibly deprecated.
507: 507:

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import type {KeyInfo} from "$lib/serial/keymap-codes"
export let action: number | KeyInfo
export let display: "inline-keys" | "keys" = "inline-keys"
$: info = typeof action === "number" ? KEYMAP_CODES[action] ?? {code: action} : action
</script>
{#if display === "keys"}
<kbd class:icon={!!info.icon}>
{info.icon ?? info.id ?? `0x${info.code.toString(16)}`}
</kbd>
{:else if display === "inline-keys"}
{#if !info.icon && info.id?.length === 1}
<span>{info.id}</span>
{:else}
<kbd class="inline-kbd" class:icon={!!info.icon}
>{info.icon ?? info.id ?? `0x${info.code.toString(16)}`}</kbd
>
{/if}
{/if}
<style lang="scss">
kbd:not(.inline-kbd) {
height: 24px;
padding-block: auto;
transition: color 250ms ease;
}
.inline-kbd {
margin-inline-end: 2px;
}
:global(span) + .inline-kbd {
margin-inline-start: 2px;
}
</style>

View File

@@ -35,6 +35,7 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
height: auto;
margin: 0; margin: 0;
padding: 8px; padding: 8px;
@@ -61,4 +62,8 @@
text-align: start; text-align: start;
} }
kbd {
height: 24px;
}
</style> </style>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import Action from "$lib/components/Action.svelte"
import type {KeyInfo} from "$lib/serial/keymap-codes"
export let actions: Array<number | KeyInfo>
export let display: "keys" | "inline-keys" = "inline-keys"
</script>
{#each actions as action, i (`${typeof action === "number" ? action : action.code}:${i}`)}
<Action {action} {display} />
{/each}

View File

@@ -1,8 +0,0 @@
import {Extension, Node} from "@tiptap/core"
const CharaAction = Node.create({
name: "Action",
renderHTML({HTMLAttributes}) {
return ["kbd", HTMLAttributes, 0]
},
})

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {createEventDispatcher} from "svelte" import {createEventDispatcher} from "svelte"
import Dialog from "$lib/dialogs/Dialog.svelte"
export let title: string export let title: string
export let message: string | undefined export let message: string | undefined
@@ -7,15 +8,9 @@
export let confirmTitle: string export let confirmTitle: string
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export function show() {
modal.showModal()
}
let modal: HTMLDialogElement
</script> </script>
<dialog bind:this={modal}> <Dialog>
<h1>{@html title}</h1> <h1>{@html title}</h1>
{#if message} {#if message}
<p>{@html message}</p> <p>{@html message}</p>
@@ -24,7 +19,7 @@
<button on:click={() => dispatch("abort")}>{abortTitle}</button> <button on:click={() => dispatch("abort")}>{abortTitle}</button>
<button class="primary" on:click={() => dispatch("confirm")}>{confirmTitle}</button> <button class="primary" on:click={() => dispatch("confirm")}>{confirmTitle}</button>
</div> </div>
</dialog> </Dialog>
<style lang="scss"> <style lang="scss">
h1 { h1 {

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import {onMount} from "svelte"
onMount(() => {
modal.showModal()
})
let modal: HTMLDialogElement
</script>
<dialog bind:this={modal}>
<slot />
</dialog>
<style lang="scss">
dialog {
min-width: 300px;
max-width: 512px;
color: var(--md-sys-color-on-background);
background: var(--md-sys-color-background);
border: none;
border-radius: 38px;
box-shadow: 0 0 48px rgba(0 0 0 / 60%);
}
dialog::backdrop {
opacity: 0.5;
background: black;
}
</style>

View File

@@ -0,0 +1,161 @@
<script lang="ts">
import Dialog from "$lib/dialogs/Dialog.svelte"
import type {Change, ChordChange, LayoutChange, SettingChange} from "$lib/undo-redo"
import {ChangeType, chords} from "$lib/undo-redo"
import ActionString from "$lib/components/ActionString.svelte"
import LL from "../../i18n/i18n-svelte"
import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
export let changes: Change[] = [
{type: ChangeType.Layout, layer: 0, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Layout, layer: 1, id: 1, action: 1},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Setting, id: 0, setting: 2},
{type: ChangeType.Chord, id: [1], actions: [55], phrase: [55, 63, 37, 36]},
{
type: ChangeType.Chord,
id: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
actions: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
phrase: [55, 63, 37, 36],
},
{
type: ChangeType.Chord,
id: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
actions: [KEYMAP_IDS.get("y")!.code, KEYMAP_IDS.get("r")!.code, KEYMAP_IDS.get("t")!.code],
phrase: [],
},
]
$: existingChords = new Set($chords.map(it => JSON.stringify(it.id)))
$: layoutChanges = Array.from(
{length: 3},
(_, i) => changes.filter(it => it.type === ChangeType.Layout && it.layer === i) as LayoutChange[],
)
$: settingChanges = changes.filter(it => it.type === ChangeType.Setting) as SettingChange[]
$: chordChanges = {
added: changes.filter(
it =>
it.type === ChangeType.Chord && it.phrase.length > 0 && !existingChords.has(JSON.stringify(it.id)),
) as ChordChange[],
changed: changes.filter(
it => it.type === ChangeType.Chord && it.phrase.length > 0 && existingChords.has(JSON.stringify(it.id)),
) as ChordChange[],
deleted: changes.filter(it => it.type === ChangeType.Chord && it.phrase.length === 0) as ChordChange[],
}
$: totalChordChanges = Object.values(chordChanges).reduce((acc, curr) => acc + curr.length, 0)
</script>
<Dialog>
<h1>{$LL.changes.TITLE()}</h1>
<h2>
<label><input type="checkbox" class="checkbox" />{$LL.changes.ALL_CHANGES()}</label>
</h2>
<ul>
{#if layoutChanges.some(it => it.length > 0)}
<li>
<h3>
<label>
<input type="checkbox" class="checkbox" />
{$LL.changes.layout.TITLE(layoutChanges.reduce((acc, curr) => acc + curr.length, 0))}
</label>
</h3>
<ul>
{#each layoutChanges
.map((it, i) => /** @type {const} */ ([it, i + 1]))
.filter(([it]) => it.length > 0) as [changes, layer]}
<li>
<h4>
<label>
<input type="checkbox" class="checkbox" />
{$LL.changes.layout.LAYER({changes: changes.length, layer})}
</label>
</h4>
</li>
{/each}
</ul>
</li>
{/if}
{#if settingChanges.length > 0}
<li>
<h3>
<label
><input type="checkbox" class="checkbox" />{$LL.changes.settings.TITLE(
settingChanges.length,
)}</label
>
</h3>
</li>
{/if}
{#if totalChordChanges > 0}
<li>
<h3>
<label
><input type="checkbox" class="checkbox" />{$LL.changes.chords.TITLE(totalChordChanges)}</label
>
</h3>
<ul>
{#each Object.entries(chordChanges) as [category, changes]}
{#if changes.length > 0}
<li>
<h4>
<label
><input type="checkbox" class="checkbox" />
{#if category === "added"}
{$LL.changes.chords.NEW_CHORDS(changes.length)}
{:else if category === "changed"}
{$LL.changes.chords.CHANGED_CHORDS(changes.length)}
{:else if category === "deleted"}
{$LL.changes.chords.DELETED_CHORDS(changes.length)}
{/if}
</label>
</h4>
<ul>
{#each changes as change}
<li>
<label>
<input type="checkbox" class="checkbox" />
<ActionString display="keys" actions={change.actions} />
<ActionString actions={change.phrase} />
</label>
</li>
{/each}
</ul>
</li>
{/if}
{/each}
</ul>
</li>
{/if}
</ul>
</Dialog>
<style lang="scss">
h1 {
font-size: 2em;
text-align: center;
}
h2 {
font-size: 1.5em;
}
ul {
padding-inline-start: 0;
list-style: none;
}
li {
margin-inline-start: 24px;
}
</style>

View File

@@ -1,4 +1,4 @@
import ConfirmDialog from "$lib/ConfirmDialog.svelte" import ConfirmDialog from "$lib/dialogs/ConfirmDialog.svelte"
export async function askForConfirmation( export async function askForConfirmation(
title: string, title: string,

39
src/lib/os-layout.ts Normal file
View File

@@ -0,0 +1,39 @@
import {persistentWritable} from "$lib/storage"
import {get} from "svelte/store"
export const osLayout = persistentWritable<Record<string, string>>("os-layout", {})
const keysCurrentlyDown = new Set<string>()
function keydown({code, key}: KeyboardEvent) {
const keys = [...keysCurrentlyDown]
keysCurrentlyDown.add(code)
const keyString = JSON.stringify([...keys.sort(), code])
if (keyString in get(osLayout) || get(osLayout)[JSON.stringify([code])] === key) return
osLayout.update(layout => {
layout[keyString] = key
return layout
})
}
function keyup({code}: KeyboardEvent) {
keysCurrentlyDown.delete(code)
}
export function runLayoutDetection() {
if ("keyboard" in navigator) {
;(navigator.keyboard as any).getLayoutMap().then((layout: Map<string, string>) => {
osLayout.update(osLayout => {
Object.assign(
osLayout,
Object.fromEntries([...layout.entries()].map(([key, value]) => [JSON.stringify([key]), value])),
)
return osLayout
})
})
}
window.addEventListener("keydown", keydown)
window.addEventListener("keyup", keyup)
}

View File

@@ -45,17 +45,33 @@ export const deviceSettings = persistentWritable<number[]>(
export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done") export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done")
export interface ProgressInfo {
max: number
current: number
}
export const syncProgress = writable<ProgressInfo | undefined>(undefined)
export async function initSerial(manual = false) { export async function initSerial(manual = false) {
const device = get(serialPort) ?? new CharaDevice() const device = get(serialPort) ?? new CharaDevice()
await device.init(manual) await device.init(manual)
serialPort.set(device) serialPort.set(device)
const chordCount = await device.getChordCount()
syncStatus.set("downloading") syncStatus.set("downloading")
const max = Object.keys(settingInfo.settings).length + device.keyCount * 3 + chordCount
let current = 0
syncProgress.set({max, current})
function progressTick() {
current++
syncProgress.set({max, current})
}
const parsedSettings: number[] = [] const parsedSettings: number[] = []
for (const key in settingInfo.settings) { for (const key in settingInfo.settings) {
try { try {
parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key)) parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key))
} catch {} } catch {}
progressTick()
} }
deviceSettings.set(parsedSettings) deviceSettings.set(parsedSettings)
@@ -63,15 +79,17 @@ export async function initSerial(manual = false) {
for (let layer = 1; layer <= 3; layer++) { for (let layer = 1; layer <= 3; layer++) {
for (let i = 0; i < device.keyCount; i++) { for (let i = 0; i < device.keyCount; i++) {
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i) parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
progressTick()
} }
} }
deviceLayout.set(parsedLayout) deviceLayout.set(parsedLayout)
const chordCount = await device.getChordCount()
const chordInfo = [] const chordInfo = []
for (let i = 0; i < chordCount; i++) { for (let i = 0; i < chordCount; i++) {
chordInfo.push(await device.getChord(i)) chordInfo.push(await device.getChord(i))
progressTick()
} }
deviceChords.set(chordInfo) deviceChords.set(chordInfo)
syncStatus.set("done") syncStatus.set("done")
syncProgress.set(undefined)
} }

View File

@@ -1,5 +1,6 @@
@import "./form/button"; @import "./form/button";
@import "./form/toggle"; @import "./form/toggle";
@import "./form/checkbox";
@import "./kbd"; @import "./kbd";
@import "./print"; @import "./print";

View File

@@ -13,7 +13,6 @@ export const action: Action<HTMLElement, {title?: string; shortcut?: string}> =
arrow: false, arrow: false,
theme: "tooltip", theme: "tooltip",
animation: "fade", animation: "fade",
delay: [500, 0],
onShow(instance) { onShow(instance) {
component ??= new Tooltip({ component ??= new Tooltip({
target: instance.popper.querySelector(".tippy-content") as HTMLElement, target: instance.popper.querySelector(".tippy-content") as HTMLElement,

View File

@@ -9,7 +9,7 @@
import Navigation from "./Navigation.svelte" import Navigation from "./Navigation.svelte"
import {canAutoConnect} from "$lib/serial/device" import {canAutoConnect} from "$lib/serial/device"
import {initSerial} from "$lib/serial/connection" import {initSerial} from "$lib/serial/connection"
import type {LayoutServerData} from "./$types" import type {LayoutData} from "./$types"
import {browser} from "$app/environment" import {browser} from "$app/environment"
import BrowserWarning from "./BrowserWarning.svelte" import BrowserWarning from "./BrowserWarning.svelte"
import "tippy.js/animations/shift-away.css" import "tippy.js/animations/shift-away.css"
@@ -21,12 +21,16 @@
import {detectLocale} from "../i18n/i18n-util" import {detectLocale} from "../i18n/i18n-util"
import type {Locales} from "../i18n/i18n-types" import type {Locales} from "../i18n/i18n-types"
import Footer from "./Footer.svelte" import Footer from "./Footer.svelte"
import {runLayoutDetection} from "$lib/os-layout.js"
import PageTransition from "./PageTransition.svelte"
import SyncOverlay from "./SyncOverlay.svelte"
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale() const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
loadLocale(locale) loadLocale(locale)
setLocale(locale) setLocale(locale)
if (browser) { if (browser) {
runLayoutDetection()
tippy.setDefaultProps({ tippy.setDefaultProps({
animation: "shift-away", animation: "shift-away",
theme: "surface-variant", theme: "surface-variant",
@@ -37,7 +41,7 @@
}) })
} }
export let data: LayoutServerData export let data: LayoutData
onMount(async () => { onMount(async () => {
theme.subscribe(it => { theme.subscribe(it => {
@@ -63,11 +67,15 @@
<meta name="theme-color" content={data.themeColor} /> <meta name="theme-color" content={data.themeColor} />
</svelte:head> </svelte:head>
<SyncOverlay />
<Navigation /> <Navigation />
<main> <!-- <PickChangesDialog /> -->
<PageTransition>
<slot /> <slot />
</main> </PageTransition>
<Footer /> <Footer />

View File

@@ -4,8 +4,15 @@
import type {Change} from "$lib/undo-redo" import type {Change} from "$lib/undo-redo"
import {fly} from "svelte/transition" import {fly} from "svelte/transition"
import {action} from "$lib/title" import {action} from "$lib/title"
import {deviceChords, deviceLayout, deviceSettings, serialPort, syncStatus} from "$lib/serial/connection" import {
import {askForConfirmation} from "$lib/confirm-dialog" deviceChords,
deviceLayout,
deviceSettings,
serialPort,
syncProgress,
syncStatus,
} from "$lib/serial/connection"
import {askForConfirmation} from "$lib/dialogs/confirm-dialog"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes" import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
function undo(event: MouseEvent) { function undo(event: MouseEvent) {
@@ -94,7 +101,23 @@
// would be if they click it every time they change a setting. // would be if they click it every time they change a setting.
// Because of that, we don't need to show a fearmongering message such as // Because of that, we don't need to show a fearmongering message such as
// "Your device will break after you click this 10,000 times!" // "Your device will break after you click this 10,000 times!"
await new Promise(resolve => setTimeout(resolve, 6000)) const virtualWriteTime = 6000
const startStamp = performance.now()
await new Promise<void>(resolve => {
function animate() {
const delta = performance.now() - startStamp
syncProgress.set({
max: virtualWriteTime,
current: delta,
})
if (delta >= virtualWriteTime) {
resolve()
} else {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
})
if ($serialPort) { if ($serialPort) {
await $serialPort.commit() await $serialPort.commit()
$changes = [] $changes = []

View File

@@ -1,5 +1,34 @@
<script> <script lang="ts">
import {version} from "$app/environment" import {browser, version} from "$app/environment"
import {action} from "$lib/title"
import LL, {setLocale} from "../i18n/i18n-svelte"
import {theme} from "$lib/preferences.js"
import type {Locales} from "../i18n/i18n-types"
import {detectLocale, locales} from "../i18n/i18n-util"
import {loadLocaleAsync} from "../i18n/i18n-util.async"
import {tick} from "svelte"
let locale = (browser && (localStorage.getItem("locale") as Locales)) || detectLocale()
$: if (browser)
(async () => {
localStorage.setItem("locale", locale)
await loadLocaleAsync(locale)
setLocale(locale)
})()
function switchTheme() {
const mode = $theme.mode === "light" ? "dark" : "light"
if (document.startViewTransition) {
document.startViewTransition(async () => {
$theme.mode = mode
await tick()
})
} else {
$theme.mode = mode
}
}
let languageSelect: HTMLSelectElement
</script> </script>
<footer> <footer>
@@ -13,22 +42,107 @@
> >
</li> </li>
</ul> </ul>
<ul>
<li>
<input use:action={{title: $LL.profile.theme.COLOR_SCHEME()}} type="color" bind:value={$theme.color} />
</li>
<li>
{#if $theme.mode === "light"}
<button use:action={{title: $LL.profile.theme.DARK_MODE()}} class="icon" on:click={switchTheme}>
dark_mode
</button>
{:else if $theme.mode === "dark"}
<button use:action={{title: $LL.profile.theme.LIGHT_MODE()}} class="icon" on:click={switchTheme}>
light_mode
</button>
{/if}
</li>
<li>
<button
class="icon"
use:action={{title: $LL.profile.LANGUAGE()}}
on:click={() => languageSelect.click()}
>translate
<select bind:value={locale} bind:this={languageSelect}>
{#each locales as code}
<option value={code}>{code}</option>
{/each}
</select>
</button>
</li>
</ul>
</footer> </footer>
<style> <style lang="scss">
select {
position: absolute;
opacity: 0;
}
input[type="color"] {
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
inline-size: 20px;
block-size: 20px;
margin: 0;
padding: 0;
color: inherit;
background: transparent;
border: none;
border-radius: 50%;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
}
}
footer { footer {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 16px;
opacity: 0.4; opacity: 0.4;
} }
ul { ul {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: center;
margin: 0;
padding: 0;
list-style: none; list-style: none;
} }
ul:last-child {
gap: 12px;
button {
height: 24px;
font-size: 20px;
}
}
a { a {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -8,8 +8,8 @@
import {canAutoConnect} from "$lib/serial/device" import {canAutoConnect} from "$lib/serial/device"
import {browser} from "$app/environment" import {browser} from "$app/environment"
import {userPreferences} from "$lib/preferences" import {userPreferences} from "$lib/preferences"
import {action} from "$lib/title"
import LL from "../i18n/i18n-svelte" import LL from "../i18n/i18n-svelte"
import Profile from "./Profile.svelte"
import ConfigTabs from "./ConfigTabs.svelte" import ConfigTabs from "./ConfigTabs.svelte"
import EditActions from "./EditActions.svelte" import EditActions from "./EditActions.svelte"
@@ -29,8 +29,18 @@
<div class="actions"> <div class="actions">
{#if $canShare} {#if $canShare}
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button> <button
<button transition:fly={{x: -8}} class="icon" on:click={() => print()}>print</button> use:action={{title: $LL.share.TITLE()}}
transition:fly={{x: -8}}
class="icon"
on:click={triggerShare}>share</button
>
<button
use:action={{title: $LL.print.TITLE()}}
transition:fly={{x: -8}}
class="icon"
on:click={() => print()}>print</button
>
<div transition:slide class="separator" /> <div transition:slide class="separator" />
{/if} {/if}
{#if import.meta.env.TAURI_FAMILY === undefined} {#if import.meta.env.TAURI_FAMILY === undefined}
@@ -39,90 +49,31 @@
{/await} {/await}
{/if} {/if}
{#if $serialPort} {#if $serialPort}
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}"> <button use:action={{title: $LL.backup.TITLE()}} use:popup={BackupPopup} class="icon {$syncStatus}">
{#if $syncStatus === "downloading"} {#if $userPreferences.backup}
backup history
{:else if $syncStatus === "uploading"}
cloud_download
{:else if $userPreferences.backup}
cloud_done
{:else} {:else}
cloud_off history_toggle_off
{/if} {/if}
</button> </button>
{/if} {/if}
<button <button
bind:this={connectButton} bind:this={connectButton}
title="Devices" use:action={{title: $LL.deviceManager.TITLE()}}
use:popup={ConnectionPopup} use:popup={ConnectionPopup}
class="icon connect" class="icon connect"
class:error={$serialPort === undefined} class:error={$serialPort === undefined}
> >
cable cable
</button> </button>
<button title={$LL.profile.TITLE()} use:popup={Profile} class="icon account">person</button>
</div> </div>
</nav> </nav>
<style lang="scss"> <style lang="scss">
@keyframes sync {
0% {
scale: 1 1;
opacity: 1;
}
85% {
scale: 1 0;
opacity: 1;
}
86% {
scale: 1 1;
opacity: 0;
}
100% {
scale: 1 1;
opacity: 1;
}
}
.uploading::after,
.downloading::after {
content: "";
position: absolute;
top: 20px;
left: 50%;
transform-origin: top;
translate: -50% 0;
width: 8px;
height: 10px;
background: var(--md-sys-color-background);
animation: sync 1s linear infinite;
}
.uploading::after {
transform-origin: bottom;
}
.downloading.active::after,
.uploading.active::after {
background: var(--md-sys-color-primary);
}
.sync.downloading::after {
top: 10px;
transform-origin: bottom;
border-radius: 4px;
}
.separator { .separator {
width: 1px; width: 1px;
height: 24px; height: 24px;
margin-inline: 4px;
background: var(--md-sys-color-outline-variant); background: var(--md-sys-color-outline-variant);
} }
@@ -184,12 +135,6 @@
} }
} }
.icon.account {
font-size: 32px;
color: var(--md-sys-color-on-secondary-container);
background: var(--md-sys-color-secondary-container);
}
:disabled { :disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import {fly} from "svelte/transition"
import {afterNavigate, beforeNavigate} from "$app/navigation"
import {expoIn, expoOut, quadIn, quadOut} from "svelte/easing"
let inDirection = 0
let outDirection = 0
let outroEnd: undefined | (() => void) = undefined
let animationDone: Promise<void>
let isNavigating = false
const routeOrder = ["/config/chords/", "/config/layout/", "/config/settings/"]
beforeNavigate(navigation => {
const from = navigation.from?.url.pathname
const to = navigation.to?.url.pathname
isNavigating = true
if (!(from && to && routeOrder.includes(from) && routeOrder.includes(to))) {
inDirection = 0
outDirection = 0
return
}
const fromIndex = routeOrder.indexOf(from)
const toIndex = routeOrder.indexOf(to)
inDirection = fromIndex > toIndex ? -1 : 1
outDirection = fromIndex > toIndex ? 1 : -1
animationDone = new Promise(resolve => {
outroEnd = resolve
})
})
afterNavigate(async () => {
await animationDone
isNavigating = false
})
</script>
{#if !isNavigating}
<main
in:fly={{x: inDirection * 24, duration: 150, easing: expoOut}}
out:fly={{x: outDirection * 24, duration: 150, easing: expoIn}}
on:outroend={outroEnd}
>
<slot />
</main>
{/if}

View File

@@ -1,116 +0,0 @@
<script lang="ts">
import LL, {setLocale} from "../i18n/i18n-svelte"
import {theme} from "$lib/preferences"
import {tick} from "svelte"
import {detectLocale, locales} from "../i18n/i18n-util"
import {loadLocaleAsync} from "../i18n/i18n-util.async"
import type {Locales} from "../i18n/i18n-types"
let locale = (localStorage.getItem("locale") as Locales) || detectLocale()
$: (async () => {
localStorage.setItem("locale", locale)
await loadLocaleAsync(locale)
setLocale(locale)
})()
function switchTheme() {
const mode = $theme.mode === "light" ? "dark" : "light"
if (document.startViewTransition) {
document.startViewTransition(async () => {
$theme.mode = mode
await tick()
})
} else {
$theme.mode = mode
}
}
</script>
<section>
<h2>{$LL.profile.TITLE()}</h2>
<fieldset>
<legend>
<span class="icon">format_paint</span>
{$LL.profile.theme.TITLE()}
</legend>
<input title={$LL.profile.theme.COLOR_SCHEME()} type="color" bind:value={$theme.color} />
<button
title={$theme.mode === "light" ? $LL.profile.theme.LIGHT_MODE() : $LL.profile.theme.DARK_MODE()}
class="icon"
on:click={switchTheme}
>
{#if $theme.mode === "light"}
light_mode
{:else if $theme.mode === "dark"}
dark_mode
{:else}
TODO
{/if}
</button>
</fieldset>
<fieldset>
<legend>
<span class="icon">translate</span>
{$LL.profile.LANGUAGE()}
</legend>
{#each locales as code}
<label>{code}<input bind:group={locale} type="radio" value={code} name="language" /></label>
{/each}
</fieldset>
</section>
<style lang="scss">
h2 {
grid-column: 1 / span 2;
}
section {
display: grid;
grid-template-columns: auto auto;
min-width: 300px;
}
fieldset {
display: flex;
justify-content: space-around;
border: 1px solid var(--md-sys-color-outline);
border-radius: 16px;
}
legend {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
}
button,
input[type="color"] {
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
inline-size: 24px;
block-size: 24px;
margin: 0;
padding: 0;
color: inherit;
background: transparent;
border: none;
border-radius: 50%;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
}
}
</style>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import {syncProgress, syncStatus} from "$lib/serial/connection"
import LL from "../i18n/i18n-svelte"
$: if (dialog) toggleDialog($syncStatus)
async function toggleDialog(status: "uploading" | "downloading" | string) {
// debounce
await new Promise(resolve => setTimeout(resolve, 150))
if ($syncStatus !== status) return
if (!dialog.open && ($syncStatus === "uploading" || $syncStatus === "downloading")) {
message = $syncStatus
dialog.showModal()
dialog.animate([{opacity: 0}, {opacity: 1}], {duration: 250, easing: "ease"})
} else if (dialog.open) {
const animation = dialog.animate([{opacity: 1}, {opacity: 0}], {duration: 250, easing: "ease"})
animation.addEventListener("finish", () => {
dialog.close()
})
}
}
let message: "downloading" | "uploading"
let dialog: HTMLDialogElement
</script>
<dialog bind:this={dialog}>
{#if message === "downloading"}
<h2>{$LL.sync.TITLE_READ()}</h2>
{:else}
<h2>{$LL.sync.TITLE_WRITE()}</h2>
<p>{$LL.sync.DISCLAIMER_WRITE()}</p>
{/if}
<progress max={$syncProgress?.max ?? 1} value={$syncProgress?.current ?? 1}></progress>
</dialog>
<style lang="scss">
dialog::backdrop {
background: rgba(0 0 0 / 70%);
}
progress {
overflow: hidden;
width: 100%;
height: 16px;
border-radius: 8px;
}
progress::-webkit-progress-bar {
background: var(--md-sys-color-background);
}
progress::-webkit-progress-value {
background: var(--md-sys-color-primary);
}
dialog {
max-width: 14cm;
padding: 2cm;
color: white;
background: none;
border: none;
outline: none;
}
</style>

View File

@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import {KEYMAP_CODES, KEYMAP_IDS} from "$lib/serial/keymap-codes" import {KEYMAP_IDS} from "$lib/serial/keymap-codes"
import type {ChordInfo} from "$lib/undo-redo" import type {ChordInfo} from "$lib/undo-redo"
import {changes, ChangeType} from "$lib/undo-redo" import {changes, ChangeType} from "$lib/undo-redo"
import {createEventDispatcher} from "svelte" import {createEventDispatcher} from "svelte"
import LL from "../../../i18n/i18n-svelte" import LL from "../../../i18n/i18n-svelte"
import ActionString from "$lib/components/ActionString.svelte"
export let chord: ChordInfo | undefined = undefined export let chord: ChordInfo | undefined = undefined
@@ -33,7 +34,7 @@
changes.push({ changes.push({
type: ChangeType.Chord, type: ChangeType.Chord,
id: chord!.id, id: chord!.id,
actions: [...pressedKeys], actions: [...pressedKeys].sort(),
phrase: chord!.phrase, phrase: chord!.phrase,
}) })
return changes return changes
@@ -53,12 +54,7 @@
{:else if !editing && !chord} {:else if !editing && !chord}
<span>{$LL.configure.chords.NEW_CHORD()}</span> <span>{$LL.configure.chords.NEW_CHORD()}</span>
{/if} {/if}
{#each editing ? [...pressedKeys].sort() : chord?.actions ?? [] as actionId} <ActionString display="keys" actions={editing ? [...pressedKeys].sort() : chord?.actions ?? []} />
{@const {icon, id, code} = KEYMAP_CODES[actionId] ?? {code: actionId}}
<kbd class:icon={!!icon}>
{icon ?? id ?? `0x${code.toString(16)}`}
</kbd>
{/each}
<sup></sup> <sup></sup>
</button> </button>
@@ -87,12 +83,6 @@
} }
} }
kbd {
height: 24px;
padding-block: auto;
transition: color 250ms ease;
}
button::after { button::after {
content: ""; content: "";

View File

@@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import {KEYMAP_CODES, KEYMAP_IDS, specialKeycodes} from "$lib/serial/keymap-codes" import {KEYMAP_IDS, specialKeycodes} from "$lib/serial/keymap-codes"
import {tick} from "svelte" import {tick} from "svelte"
import ActionSelector from "$lib/components/layout/ActionSelector.svelte" import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
import {changes, ChangeType} from "$lib/undo-redo" import {changes, ChangeType} from "$lib/undo-redo"
import type {ChordInfo} from "$lib/undo-redo" import type {ChordInfo} from "$lib/undo-redo"
import {scale} from "svelte/transition" import {scale} from "svelte/transition"
import ActionString from "$lib/components/ActionString.svelte"
export let chord: ChordInfo export let chord: ChordInfo
@@ -149,14 +150,7 @@
<div /> <div />
<!-- placeholder for cursor placement --> <!-- placeholder for cursor placement -->
{/if} {/if}
{#each chord.phrase as actionId, i (`${actionId}:${i}`)} <ActionString actions={chord.phrase} />
{@const {icon, id, code} = KEYMAP_CODES[actionId] ?? {code: actionId}}
{#if !icon && id?.length === 1}
<span>{id}</span>
{:else}
<kbd class:icon={!!icon}>{icon ?? id ?? `0x${code.toString(16)}`}</kbd>
{/if}
{/each}
<sup></sup> <sup></sup>
</div> </div>
@@ -203,14 +197,6 @@
} }
} }
:not(.cursor) + kbd {
margin-inline-start: 2px;
}
kbd + * {
margin-inline-start: 2px;
}
[role="textbox"] { [role="textbox"] {
cursor: text; cursor: text;