Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)

allow multiple anki note loadouts (#1930)

* wip

* wikp2

* wip3

* square icons

* add biq square

* wip4

* remove input end tag

* diamonds

* finish diamonds

* plus button visual

* start new note

* more

* delete modal works

* adding and deleting

* defaults and stuff

* start tags and flags

* line

* first item horizontal and reversed

* css

* badge

* remove modeKanji etc

* hotkeys

* wip

* something

* fix hotkeys

* undo backup controller change

* update playwright selector

* fixes

* fixes

* fixes

* extra css

* fix handlebars

* fix value options not changing on type change

* full screen

* no minimizing

* move delete button to footer

* throw on more than 5 notes

* don't add kanji note for most languages

* remove redundant data

* connection line css

* hotkeys rework

* delete commented out line

* add max notes modal

* renames

* save wo formatting

* more renames

* undo a rename

* rename

* fix test

* make delete format button advanced

* rename

* more renames

authored by

Stefan Vuković and committed by
GitHub
70bead00 5abca378

+1421 -928
+53 -9
ext/css/display.css
··· 645 645 content: ''; 646 646 display: block; 647 647 } 648 + .note-actions-container { 649 + display: flex; 650 + flex-flow: row nowrap; 651 + align-items: start; 652 + } 653 + .action-button-container { 654 + display: flex; 655 + flex-direction: column; 656 + position: relative; 657 + } 658 + 659 + .note-actions-container .action-button-container::after { 660 + content: ''; 661 + position: absolute; 662 + left: calc(50% - 0.5px); 663 + top: calc((var(--action-button-size) + var(--action-button-padding) * 2) * 0.5); 664 + bottom: calc((var(--action-button-size) + var(--action-button-padding) * 2) * 0.5); 665 + width: 1px; 666 + background: var(--accent-color); 667 + pointer-events: none; 668 + opacity: 0.25; 669 + z-index: -1; 670 + } 671 + 672 + .note-actions-container .action-button-container:first-child { 673 + flex-direction: row-reverse; 674 + } 675 + .note-actions-container .action-button-container:first-child::after { 676 + top: calc(50% - 0.5px); 677 + left: calc((var(--action-button-size) + var(--action-button-padding) * 2) * 0.5); 678 + right: calc((var(--action-button-size) + var(--action-button-padding) * 2) * 0.5); 679 + bottom: auto; 680 + height: 1px; 681 + width: auto; 682 + } 683 + 684 + .note-actions-container .action-button-container:only-child::after { 685 + display: none; 686 + } 687 + 648 688 button.action-button { 649 689 cursor: pointer; 650 690 display: block; ··· 656 696 font-size: inherit; 657 697 box-shadow: none; 658 698 position: relative; 699 + z-index: 1; 659 700 transition: 660 701 opacity var(--animation-duration) linear, 661 702 visibility 0s linear 0s, ··· 701 742 box-shadow: none; 702 743 } 703 744 .icon[data-icon=view-note] { background-image: url('/images/view-note.svg'); } 704 - .icon[data-icon=add-term-kanji] { background-image: url('/images/add-term-kanji.svg'); } 705 - .icon[data-icon=add-term-kana] { background-image: url('/images/add-term-kana.svg'); } 706 - .icon[data-icon=add-kanji] { background-image: url('/images/add-term-kanji.svg'); } 707 - .icon[data-icon=overwrite-term-kanji] { background-image: url('/images/overwrite-term-kanji.svg'); } 708 - .icon[data-icon=overwrite-term-kana] { background-image: url('/images/overwrite-term-kana.svg'); } 709 - .icon[data-icon=overwrite-kanji] { background-image: url('/images/overwrite-term-kanji.svg'); } 710 - .icon[data-icon=add-duplicate-term-kanji] { background-image: url('/images/add-duplicate-term-kanji.svg'); } 711 - .icon[data-icon=add-duplicate-term-kana] { background-image: url('/images/add-duplicate-term-kana.svg'); } 712 - .icon[data-icon=add-duplicate-kanji] { background-image: url('/images/add-duplicate-term-kanji.svg'); } 745 + .icon[data-icon=big-circle] { background-image: url('/images/big-circle.svg'); } 746 + .icon[data-icon=small-circle] { background-image: url('/images/small-circle.svg'); } 747 + .icon[data-icon=big-square] { background-image: url('/images/big-square.svg'); } 748 + .icon[data-icon=big-diamond] { background-image: url('/images/big-diamond.svg'); } 749 + .icon[data-icon=overwrite-big-circle] { background-image: url('/images/overwrite-big-circle.svg'); } 750 + .icon[data-icon=overwrite-small-circle] { background-image: url('/images/overwrite-small-circle.svg'); } 751 + .icon[data-icon=overwrite-big-square] { background-image: url('/images/overwrite-big-square.svg'); } 752 + .icon[data-icon=overwrite-big-diamond] { background-image: url('/images/overwrite-big-diamond.svg'); } 753 + .icon[data-icon=add-duplicate-big-circle] { background-image: url('/images/add-duplicate-big-circle.svg'); } 754 + .icon[data-icon=add-duplicate-small-circle] { background-image: url('/images/add-duplicate-small-circle.svg'); } 755 + .icon[data-icon=add-duplicate-big-square] { background-image: url('/images/add-duplicate-big-square.svg'); } 756 + .icon[data-icon=add-duplicate-big-diamond] { background-image: url('/images/add-duplicate-big-diamond.svg'); } 713 757 .icon[data-icon=play-audio] { background-image: url('/images/play-audio.svg'); } 714 758 .icon[data-icon=source-term] { background-image: url('/images/source-term.svg'); } 715 759 .icon[data-icon=entry-current] { background-image: url('/images/entry-current.svg'); }
+62 -15
ext/css/settings.css
··· 1212 1212 1213 1213 1214 1214 /* Tabs */ 1215 - .tabs-container { 1215 + .anki-card-tabs-container { 1216 1216 display: flex; 1217 1217 flex-flow: row nowrap; 1218 1218 align-items: stretch; 1219 - margin-left: calc(-1 * var(--modal-padding-horizontal)); 1220 - margin-right: calc(-1 * var(--modal-padding-horizontal)); 1221 1219 position: relative; 1220 + width: 100%; 1221 + box-sizing: border-box; 1222 1222 } 1223 1223 .tabs { 1224 1224 flex: 1 1 auto; ··· 1227 1227 align-items: stretch; 1228 1228 height: 2.75em; 1229 1229 overflow: hidden; 1230 + min-width: 0; 1230 1231 } 1231 1232 .tab { 1232 1233 cursor: pointer; 1233 1234 flex: 1 1 auto; 1235 + min-width: 0; 1234 1236 } 1235 1237 .tab>input[type='radio'] { 1236 1238 opacity: 0; ··· 1316 1318 .tab>input[type='radio']:checked~.tab-inner>.tab-label { 1317 1319 color: var(--accent-color); 1318 1320 } 1319 - .tabs-right { 1321 + #anki-cards-new-format { 1322 + height: 2.75em; 1320 1323 flex: 0 0 auto; 1321 - flex-flow: row nowrap; 1322 - align-items: center; 1323 - justify-content: center; 1324 - padding: 0 var(--modal-padding-horizontal); 1325 - height: 2.75em; 1324 + width: 2.75em; 1325 + margin-left: auto; 1326 + cursor: pointer; 1327 + position: relative; 1328 + box-sizing: border-box; 1329 + } 1330 + 1331 + #anki-cards-new-format::before { 1332 + content: ''; 1333 + display: block; 1334 + position: absolute; 1335 + pointer-events: none; 1336 + top: 0; 1337 + left: 0; 1338 + right: 0; 1339 + bottom: 0; 1340 + background-color: var(--text-color-light3); 1341 + opacity: 0; 1342 + transition: 1343 + background-color var(--animation-duration) ease-in-out, 1344 + opacity var(--animation-duration) ease-in-out; 1345 + } 1346 + 1347 + #anki-cards-new-format:hover::before { 1348 + opacity: 0.125; 1349 + } 1350 + 1351 + #anki-cards-new-format:active::before { 1352 + opacity: 0.25; 1353 + } 1354 + 1355 + #anki-cards-new-format .icon { 1356 + background-color: var(--text-color-light3); 1357 + transition: background-color var(--animation-duration) ease-in-out; 1326 1358 } 1327 - .tabs-right:not([hidden]) { 1328 - display: flex; 1359 + 1360 + #anki-cards-new-format .icon-button>.icon-button-inner::after { 1361 + display: none; 1329 1362 } 1330 1363 1331 1364 ··· 1593 1626 margin-left: 0.25em; 1594 1627 } 1595 1628 1629 + .icon[data-icon=big-circle] { background-image: url('/images/big-circle.svg'); } 1630 + .icon[data-icon=small-circle] { background-image: url('/images/small-circle.svg'); } 1631 + .icon[data-icon=big-square] { background-image: url('/images/big-square.svg'); } 1632 + .icon[data-icon=big-diamond] { background-image: url('/images/big-diamond.svg'); } 1633 + .icon.select-icon { 1634 + padding-left: 30px; 1635 + background-repeat: no-repeat; 1636 + background-position: 5px center; 1637 + background-size: 20px 20px; 1638 + } 1639 + 1596 1640 .anki-card-fields { 1597 1641 display: grid; 1598 1642 grid-template-columns: auto 1fr; ··· 1603 1647 gap: 0.5em; 1604 1648 } 1605 1649 1606 - .anki-card-field-name-header { 1607 - font-weight: bold; 1608 - margin-right: 1em; 1609 - } 1610 1650 .anki-card-field-input-header { 1611 1651 font-weight: bold; 1612 1652 } ··· 1649 1689 display: block; 1650 1690 } 1651 1691 1692 + .anki-card-delete-format { 1693 + display: flex; 1694 + flex-flow: row nowrap; 1695 + align-items: center; 1696 + justify-content: flex-end; 1697 + padding: 0.5em 0; 1698 + } 1652 1699 1653 1700 input.anki-card-field-value { 1654 1701 flex: 1 1 auto;
+78 -67
ext/data/schemas/options-schema.json
··· 923 923 "server", 924 924 "tags", 925 925 "screenshot", 926 - "terms", 927 - "kanji", 926 + "cardFormats", 928 927 "duplicateScope", 929 928 "duplicateScopeCheckAllModels", 930 929 "checkForDuplicates", 930 + "duplicateBehavior", 931 931 "fieldTemplates", 932 932 "suspendNewCards", 933 933 "displayTagsAndFlags", ··· 973 973 } 974 974 } 975 975 }, 976 - "terms": { 977 - "type": "object", 978 - "required": [ 979 - "deck", 980 - "model", 981 - "fields" 982 - ], 983 - "properties": { 984 - "deck": { 985 - "type": "string", 986 - "default": "" 987 - }, 988 - "model": { 989 - "type": "string", 990 - "default": "" 991 - }, 992 - "fields": { 993 - "type": "object", 994 - "additionalProperties": { 976 + "cardFormats": { 977 + "type": "array", 978 + "items": { 979 + "type": "object", 980 + "required": [ 981 + "name", 982 + "icon", 983 + "deck", 984 + "model", 985 + "fields", 986 + "type" 987 + ], 988 + "properties": { 989 + "name": { 990 + "type": "string", 991 + "default": "" 992 + }, 993 + "icon": { 994 + "type": "string", 995 + "enum": ["big-circle", "small-circle", "big-square", "big-diamond"], 996 + "default": "big-circle" 997 + }, 998 + "deck": { 999 + "type": "string", 1000 + "default": "" 1001 + }, 1002 + "model": { 1003 + "type": "string", 1004 + "default": "" 1005 + }, 1006 + "type": { 1007 + "type": "string", 1008 + "enum": ["term", "kanji"], 1009 + "default": "term" 1010 + }, 1011 + "fields": { 995 1012 "type": "object", 996 - "properties": { 997 - "value:": { 998 - "type": "string", 999 - "default": "" 1000 - }, 1001 - "overwriteMode": { 1002 - "type": "string", 1003 - "enum": ["coalesce", "coalesce-new", "skip", "append", "prepend", "overwrite"], 1004 - "default": "coalesce" 1013 + "additionalProperties": { 1014 + "type": "object", 1015 + "properties": { 1016 + "value": { 1017 + "type": "string", 1018 + "default": "" 1019 + }, 1020 + "overwriteMode": { 1021 + "type": "string", 1022 + "enum": ["coalesce", "coalesce-new", "skip", "append", "prepend", "overwrite"], 1023 + "default": "coalesce" 1024 + } 1005 1025 } 1006 1026 } 1007 1027 } 1008 1028 } 1009 - } 1010 - }, 1011 - "kanji": { 1012 - "type": "object", 1013 - "required": [ 1014 - "deck", 1015 - "model", 1016 - "fields" 1017 - ], 1018 - "properties": { 1019 - "deck": { 1020 - "type": "string", 1021 - "default": "" 1029 + }, 1030 + "default": [ 1031 + { 1032 + "name": "Expression", 1033 + "icon": "big-circle", 1034 + "deck": "", 1035 + "model": "", 1036 + "fields": {}, 1037 + "type": "term" 1022 1038 }, 1023 - "model": { 1024 - "type": "string", 1025 - "default": "" 1039 + { 1040 + "name": "Reading", 1041 + "icon": "small-circle", 1042 + "deck": "", 1043 + "model": "", 1044 + "fields": {}, 1045 + "type": "term" 1026 1046 }, 1027 - "fields": { 1028 - "type": "object", 1029 - "additionalProperties": { 1030 - "type": "object", 1031 - "properties": { 1032 - "value:": { 1033 - "type": "string", 1034 - "default": "" 1035 - }, 1036 - "overwriteMode": { 1037 - "type": "string", 1038 - "enum": ["coalesce", "coalesce-new", "skip", "append", "prepend", "overwrite"], 1039 - "default": "coalesce" 1040 - } 1041 - } 1042 - } 1047 + { 1048 + "name": "Kanji", 1049 + "icon": "big-circle", 1050 + "deck": "", 1051 + "model": "", 1052 + "fields": {}, 1053 + "type": "kanji" 1043 1054 } 1044 - } 1055 + ] 1045 1056 }, 1046 1057 "duplicateScope": { 1047 1058 "type": "string", ··· 1234 1245 {"action": "historyForward", "argument": "", "key": "KeyF", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1235 1246 {"action": "profilePrevious", "argument": "", "key": "Minus", "modifiers": ["alt"], "scopes": ["popup", "search", "web"], "enabled": true}, 1236 1247 {"action": "profileNext", "argument": "", "key": "Equal", "modifiers": ["alt"], "scopes": ["popup", "search", "web"], "enabled": true}, 1237 - {"action": "addNoteKanji", "argument": "", "key": "KeyK", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1238 - {"action": "addNoteTermKanji", "argument": "", "key": "KeyE", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1239 - {"action": "addNoteTermKana", "argument": "", "key": "KeyR", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1248 + {"action": "addNote", "argument": "0", "key": "KeyE", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1249 + {"action": "addNote", "argument": "1", "key": "KeyR", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1250 + {"action": "addNote", "argument": "2", "key": "KeyK", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1240 1251 {"action": "playAudio", "argument": "", "key": "KeyP", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1241 - {"action": "viewNotes", "argument": "", "key": "KeyV", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1252 + {"action": "viewNotes", "argument": "0", "key": "KeyV", "modifiers": ["alt"], "scopes": ["popup", "search"], "enabled": true}, 1242 1253 {"action": "copyHostSelection", "argument": "", "key": "KeyC", "modifiers": ["ctrl"], "scopes": ["popup"], "enabled": true} 1243 1254 ] 1244 1255 }
+69
ext/data/templates/anki-field-templates-upgrade-v64.handlebars
··· 1 + {{<<<<<<<}} 2 + {{#*inline "expression"}} 3 + {{~#if merge~}} 4 + {{~#if modeTermKana~}} 5 + {{~#each definition.reading~}} 6 + {{{.}}} 7 + {{~#unless @last}}、{{/unless~}} 8 + {{~else~}} 9 + {{~#each definition.expression~}} 10 + {{{.}}} 11 + {{~#unless @last}}、{{/unless~}} 12 + {{~/each~}} 13 + {{~/each~}} 14 + {{~else~}} 15 + {{~#each definition.expression~}} 16 + {{{.}}} 17 + {{~#unless @last}}、{{/unless~}} 18 + {{~/each~}} 19 + {{~/if~}} 20 + {{~else~}} 21 + {{~#if modeTermKana~}} 22 + {{~#if definition.reading~}} 23 + {{definition.reading}} 24 + {{~else~}} 25 + {{definition.expression}} 26 + {{~/if~}} 27 + {{~else~}} 28 + {{definition.expression}} 29 + {{~/if~}} 30 + {{~/if~}} 31 + {{/inline}} 32 + {{=======}} 33 + {{#*inline "expression"}} 34 + {{~#if merge~}} 35 + {{~#each definition.expression~}} 36 + {{{.}}} 37 + {{~#unless @last}}、{{/unless~}} 38 + {{~/each~}} 39 + {{~else~}} 40 + {{definition.expression}} 41 + {{~/if~}} 42 + {{/inline}} 43 + {{>>>>>>>}} 44 + 45 + {{<<<<<<<}} 46 + {{#*inline "reading"}} 47 + {{~#unless modeTermKana~}} 48 + {{~#if merge~}} 49 + {{~#each definition.reading~}} 50 + {{{.}}} 51 + {{~#unless @last}}、{{/unless~}} 52 + {{~/each~}} 53 + {{~else~}} 54 + {{~definition.reading~}} 55 + {{~/if~}} 56 + {{~/unless~}} 57 + {{/inline}} 58 + {{=======}} 59 + {{#*inline "reading"}} 60 + {{~#if merge~}} 61 + {{~#each definition.reading~}} 62 + {{{.}}} 63 + {{~#unless @last}}、{{/unless~}} 64 + {{~/each~}} 65 + {{~else~}} 66 + {{~definition.reading~}} 67 + {{~/if~}} 68 + {{/inline}} 69 + {{>>>>>>>}}
+13 -35
ext/data/templates/default-anki-field-templates.handlebars
··· 50 50 51 51 {{#*inline "expression"}} 52 52 {{~#if merge~}} 53 - {{~#if modeTermKana~}} 54 - {{~#each definition.reading~}} 55 - {{{.}}} 56 - {{~#unless @last}}、{{/unless~}} 57 - {{~else~}} 58 - {{~#each definition.expression~}} 59 - {{{.}}} 60 - {{~#unless @last}}、{{/unless~}} 61 - {{~/each~}} 62 - {{~/each~}} 63 - {{~else~}} 64 - {{~#each definition.expression~}} 65 - {{{.}}} 66 - {{~#unless @last}}、{{/unless~}} 67 - {{~/each~}} 68 - {{~/if~}} 53 + {{~#each definition.expression~}} 54 + {{{.}}} 55 + {{~#unless @last}}、{{/unless~}} 56 + {{~/each~}} 69 57 {{~else~}} 70 - {{~#if modeTermKana~}} 71 - {{~#if definition.reading~}} 72 - {{definition.reading}} 73 - {{~else~}} 74 - {{definition.expression}} 75 - {{~/if~}} 76 - {{~else~}} 77 - {{definition.expression}} 78 - {{~/if~}} 58 + {{definition.expression}} 79 59 {{~/if~}} 80 60 {{/inline}} 81 61 ··· 204 184 {{/inline}} 205 185 206 186 {{#*inline "reading"}} 207 - {{~#unless modeTermKana~}} 208 - {{~#if merge~}} 209 - {{~#each definition.reading~}} 210 - {{{.}}} 211 - {{~#unless @last}}、{{/unless~}} 212 - {{~/each~}} 213 - {{~else~}} 214 - {{~definition.reading~}} 215 - {{~/if~}} 216 - {{~/unless~}} 187 + {{~#if merge~}} 188 + {{~#each definition.reading~}} 189 + {{{.}}} 190 + {{~#unless @last}}、{{/unless~}} 191 + {{~/each~}} 192 + {{~else~}} 193 + {{~definition.reading~}} 194 + {{~/if~}} 217 195 {{/inline}} 218 196 219 197 {{#*inline "sentence"}}
+37
ext/images/add-duplicate-big-diamond.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 + <defs> 4 + <linearGradient id="inner-fill" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> 5 + <stop stop-color="#6fb558" offset="0"/> 6 + <stop stop-color="#a5db9b" offset="1"/> 7 + </linearGradient> 8 + <linearGradient id="outer-rim" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> 9 + <stop stop-color="#34812c" offset="0"/> 10 + <stop stop-color="#87b870" offset="1"/> 11 + </linearGradient> 12 + <radialGradient id="center-shadow" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> 13 + <stop stop-opacity=".28986" offset="0"/> 14 + <stop stop-opacity="0" offset="1"/> 15 + </radialGradient> 16 + <filter id="green-to-purple" x="-.038462" y="-.038462" width="1.0769" height="1.0769" color-interpolation-filters="sRGB"> 17 + <feColorMatrix type="hueRotate" values="180"/> 18 + <feColorMatrix type="saturate" values="1"/> 19 + <feColorMatrix type="hueRotate" values="0"/> 20 + </filter> 21 + 22 + <symbol id="plus-in-diamond" viewBox="0 0 16 16"> 23 + <path d="M8 2 L14 8 L8 14 L2 8 Z" fill="url(#inner-fill)"/> 24 + <path d="M8 3 L13 8 L8 13 L3 8 Z" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> 25 + <path d="M8 2 L14 8 L8 14 L2 8 Z" fill="none" stroke="url(#outer-rim)"/> 26 + <circle cx="8" cy="8" r="3" fill="url(#center-shadow)"/> 27 + <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> 28 + </symbol> 29 + </defs> 30 + 31 + <g transform="matrix(.91504 0 0 .91504 1.5 -1.5)" filter="url(#green-to-purple)"> 32 + <use xlink:href="#plus-in-diamond"/> 33 + </g> 34 + <g transform="matrix(.91504 0 0 .91504 0 3)" filter="url(#green-to-purple)"> 35 + <use xlink:href="#plus-in-diamond"/> 36 + </g> 37 + </svg>
+37
ext/images/add-duplicate-big-square.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 + <defs> 4 + <linearGradient id="inner-fill" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> 5 + <stop stop-color="#6fb558" offset="0"/> 6 + <stop stop-color="#a5db9b" offset="1"/> 7 + </linearGradient> 8 + <linearGradient id="outer-rim" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> 9 + <stop stop-color="#34812c" offset="0"/> 10 + <stop stop-color="#87b870" offset="1"/> 11 + </linearGradient> 12 + <radialGradient id="center-shadow" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> 13 + <stop stop-opacity=".28986" offset="0"/> 14 + <stop stop-opacity="0" offset="1"/> 15 + </radialGradient> 16 + <filter id="green-to-purple" x="-.038462" y="-.038462" width="1.0769" height="1.0769" color-interpolation-filters="sRGB"> 17 + <feColorMatrix type="hueRotate" values="180"/> 18 + <feColorMatrix type="saturate" values="1"/> 19 + <feColorMatrix type="hueRotate" values="0"/> 20 + </filter> 21 + 22 + <symbol id="plus-in-square" viewBox="0 0 16 16"> 23 + <rect x="1.5" y="1.5" width="13" height="13" rx="1" fill="url(#inner-fill)"/> 24 + <rect x="2.25" y="2.25" width="11.5" height="11.5" rx="0.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> 25 + <rect x="1.5" y="1.5" width="13" height="13" rx="1" fill="none" stroke="url(#outer-rim)"/> 26 + <rect x="5" y="5" width="6" height="6" rx="1" fill="url(#center-shadow)"/> 27 + <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> 28 + </symbol> 29 + </defs> 30 + 31 + <g transform="matrix(.91504 0 0 .91504 2.2745 -.91504)" filter="url(#green-to-purple)"> 32 + <use xlink:href="#plus-in-square"/> 33 + </g> 34 + <g transform="matrix(.91504 0 0 .91504 -.91504 2.2745)" filter="url(#green-to-purple)"> 35 + <use xlink:href="#plus-in-square"/> 36 + </g> 37 + </svg>
ext/images/add-duplicate-term-kana-blue.svg ext/images/add-duplicate-small-circle-blue.svg
ext/images/add-duplicate-term-kana.svg ext/images/add-duplicate-small-circle.svg
ext/images/add-duplicate-term-kanji-blue.svg ext/images/add-duplicate-big-circle-blue.svg
ext/images/add-duplicate-term-kanji.svg ext/images/add-duplicate-big-circle.svg
ext/images/add-term-kana.svg ext/images/small-circle.svg
ext/images/add-term-kanji.svg ext/images/big-circle.svg
+24
ext/images/big-diamond.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 + <defs> 4 + <linearGradient id="inner-fill" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> 5 + <stop stop-color="#6fb558" offset="0"/> 6 + <stop stop-color="#a5db9b" offset="1"/> 7 + </linearGradient> 8 + <linearGradient id="outer-rim" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> 9 + <stop stop-color="#34812c" offset="0"/> 10 + <stop stop-color="#87b870" offset="1"/> 11 + </linearGradient> 12 + <radialGradient id="center-shadow" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> 13 + <stop stop-opacity=".28986" offset="0"/> 14 + <stop stop-opacity="0" offset="1"/> 15 + </radialGradient> 16 + </defs> 17 + <g> 18 + <path d="M8 2 L14 8 L8 14 L2 8 Z" fill="url(#inner-fill)"/> 19 + <path d="M8 3 L13 8 L8 13 L3 8 Z" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> 20 + <path d="M8 2 L14 8 L8 14 L2 8 Z" fill="none" stroke="url(#outer-rim)"/> 21 + <circle cx="8" cy="8" r="3" fill="url(#center-shadow)"/> 22 + <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> 23 + </g> 24 + </svg>
+24
ext/images/big-square.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 + <defs> 4 + <linearGradient id="inner-fill" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> 5 + <stop stop-color="#6fb558" offset="0"/> 6 + <stop stop-color="#a5db9b" offset="1"/> 7 + </linearGradient> 8 + <linearGradient id="outer-rim" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> 9 + <stop stop-color="#34812c" offset="0"/> 10 + <stop stop-color="#87b870" offset="1"/> 11 + </linearGradient> 12 + <radialGradient id="center-shadow" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> 13 + <stop stop-opacity=".28986" offset="0"/> 14 + <stop stop-opacity="0" offset="1"/> 15 + </radialGradient> 16 + </defs> 17 + <g> 18 + <rect x="1.5" y="1.5" width="13" height="13" rx="1" fill="url(#inner-fill)"/> 19 + <rect x="2.25" y="2.25" width="11.5" height="11.5" rx="0.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> 20 + <rect x="1.5" y="1.5" width="13" height="13" rx="1" fill="none" stroke="url(#outer-rim)"/> 21 + <rect x="5" y="5" width="6" height="6" rx="1" fill="url(#center-shadow)"/> 22 + <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> 23 + </g> 24 + </svg>
+31
ext/images/overwrite-big-diamond.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 + <defs> 4 + <linearGradient id="inner-fill" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> 5 + <stop stop-color="#6fb558" offset="0"/> 6 + <stop stop-color="#a5db9b" offset="1"/> 7 + </linearGradient> 8 + <linearGradient id="outer-rim" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> 9 + <stop stop-color="#34812c" offset="0"/> 10 + <stop stop-color="#87b870" offset="1"/> 11 + </linearGradient> 12 + <radialGradient id="center-shadow" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> 13 + <stop stop-opacity=".28986" offset="0"/> 14 + <stop stop-opacity="0" offset="1"/> 15 + </radialGradient> 16 + <filter id="green-to-orange" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB"> 17 + <feColorMatrix result="color1" type="hueRotate" values="280"/> 18 + <feColorMatrix result="color2" type="saturate" values="2"/> 19 + </filter> 20 + </defs> 21 + <g filter="url(#green-to-orange)"> 22 + <path d="M8 2 L14 8 L8 14 L2 8 Z" fill="url(#inner-fill)"/> 23 + <path d="M8 3 L13 8 L8 13 L3 8 Z" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> 24 + <path d="M8 2 L14 8 L8 14 L2 8 Z" fill="none" stroke="url(#outer-rim)"/> 25 + <path 26 + d="m10.309 8.2795 0.66159 0.082684c-0.18327 1.4812-1.4463 2.6278-2.9771 2.6278-0.94244 0-1.7834-0.43457-2.3334-1.1143l3.11e-5 1.1143h-0.66667v-2.3333h2.3333v0.66667l-1.2482 3.453e-4c0.42167 0.60433 1.1221 0.99971 1.9149 0.99971 1.1906 0 2.173-0.89179 2.3156-2.0439zm0.68445-3.2895v2.3333h-2.3333v-0.66666l1.2484-5.09e-5c-0.42165-0.60446-1.1222-0.99995-1.9151-0.99995-1.1907 0-2.173 0.8918-2.3156 2.0439l-0.66159-0.082684c0.18327-1.4813 1.4463-2.6279 2.9771-2.6279 0.94244 0 1.7834 0.43457 2.3334 1.1143l-2.9e-5 -1.1143z" 27 + fill="#fff" 28 + style="stroke:#ffffff;stroke-opacity:1;stroke-width:0.4;stroke-dasharray:none" 29 + /> 30 + </g> 31 + </svg>
+31
ext/images/overwrite-big-square.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 + <defs> 4 + <linearGradient id="inner-fill" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> 5 + <stop stop-color="#6fb558" offset="0"/> 6 + <stop stop-color="#a5db9b" offset="1"/> 7 + </linearGradient> 8 + <linearGradient id="outer-rim" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> 9 + <stop stop-color="#34812c" offset="0"/> 10 + <stop stop-color="#87b870" offset="1"/> 11 + </linearGradient> 12 + <radialGradient id="center-shadow" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> 13 + <stop stop-opacity=".28986" offset="0"/> 14 + <stop stop-opacity="0" offset="1"/> 15 + </radialGradient> 16 + <filter id="green-to-orange" x="-.083333" y="-.083333" width="1.1667" height="1.1667" color-interpolation-filters="sRGB"> 17 + <feColorMatrix result="color1" type="hueRotate" values="280"/> 18 + <feColorMatrix result="color2" type="saturate" values="2"/> 19 + </filter> 20 + </defs> 21 + <g filter="url(#green-to-orange)"> 22 + <rect x="1.5" y="1.5" width="13" height="13" rx="1" fill="url(#inner-fill)"/> 23 + <rect x="2.25" y="2.25" width="11.5" height="11.5" rx="0.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> 24 + <rect x="1.5" y="1.5" width="13" height="13" rx="1" fill="none" stroke="url(#outer-rim)"/> 25 + <path 26 + d="m10.309 8.2795 0.66159 0.082684c-0.18327 1.4812-1.4463 2.6278-2.9771 2.6278-0.94244 0-1.7834-0.43457-2.3334-1.1143l3.11e-5 1.1143h-0.66667v-2.3333h2.3333v0.66667l-1.2482 3.453e-4c0.42167 0.60433 1.1221 0.99971 1.9149 0.99971 1.1906 0 2.173-0.89179 2.3156-2.0439zm0.68445-3.2895v2.3333h-2.3333v-0.66666l1.2484-5.09e-5c-0.42165-0.60446-1.1222-0.99995-1.9151-0.99995-1.1907 0-2.173 0.8918-2.3156 2.0439l-0.66159-0.082684c0.18327-1.4813 1.4463-2.6279 2.9771-2.6279 0.94244 0 1.7834 0.43457 2.3334 1.1143l-2.9e-5 -1.1143z" 27 + fill="#fff" 28 + style="stroke:#ffffff;stroke-opacity:1;stroke-width:0.4;stroke-dasharray:none" 29 + /> 30 + </g> 31 + </svg>
ext/images/overwrite-term-kana.svg ext/images/overwrite-small-circle.svg
ext/images/overwrite-term-kanji.svg ext/images/overwrite-big-circle.svg
+9 -10
ext/js/data/anki-note-builder.js
··· 46 46 */ 47 47 async createNote({ 48 48 dictionaryEntry, 49 - mode, 49 + cardFormat, 50 50 context, 51 51 template, 52 - deckName, 53 - modelName, 54 - fields, 55 52 tags = [], 56 53 requirements = [], 57 54 duplicateScope = 'collection', ··· 62 59 mediaOptions = null, 63 60 dictionaryStylesMap = new Map(), 64 61 }) { 62 + const {deck: deckName, model: modelName, fields: fieldsSettings} = cardFormat; 63 + const fields = Object.entries(fieldsSettings); 65 64 let duplicateScopeDeckName = null; 66 65 let duplicateScopeCheckChildren = false; 67 66 if (duplicateScope === 'deck-root') { ··· 91 90 // Ignore 92 91 } 93 92 94 - const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media, dictionaryStylesMap); 93 + const commonData = this._createData(dictionaryEntry, cardFormat, context, resultOutputMode, glossaryLayoutMode, compactTags, media, dictionaryStylesMap); 95 94 const formattedFieldValuePromises = []; 96 95 for (const [, {value: fieldValue}] of fields) { 97 96 const formattedFieldValuePromise = this._formatField(fieldValue, commonData, template); ··· 140 139 */ 141 140 async getRenderingData({ 142 141 dictionaryEntry, 143 - mode, 142 + cardFormat, 144 143 context, 145 144 resultOutputMode = 'split', 146 145 glossaryLayoutMode = 'default', ··· 148 147 marker, 149 148 dictionaryStylesMap, 150 149 }) { 151 - const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, void 0, dictionaryStylesMap); 150 + const commonData = this._createData(dictionaryEntry, cardFormat, context, resultOutputMode, glossaryLayoutMode, compactTags, void 0, dictionaryStylesMap); 152 151 return await this._templateRenderer.getModifiedData({marker, commonData}, 'ankiNote'); 153 152 } 154 153 ··· 202 201 203 202 /** 204 203 * @param {import('dictionary').DictionaryEntry} dictionaryEntry 205 - * @param {import('anki-templates-internal').CreateMode} mode 204 + * @param {import('settings').AnkiCardFormat} cardFormat 206 205 * @param {import('anki-templates-internal').Context} context 207 206 * @param {import('settings').ResultOutputMode} resultOutputMode 208 207 * @param {import('settings').GlossaryLayoutMode} glossaryLayoutMode ··· 211 210 * @param {Map<string, string>} dictionaryStylesMap 212 211 * @returns {import('anki-note-builder').CommonData} 213 212 */ 214 - _createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media, dictionaryStylesMap) { 213 + _createData(dictionaryEntry, cardFormat, context, resultOutputMode, glossaryLayoutMode, compactTags, media, dictionaryStylesMap) { 215 214 return { 216 215 dictionaryEntry, 217 - mode, 216 + cardFormat, 218 217 context, 219 218 resultOutputMode, 220 219 glossaryLayoutMode,
-4
ext/js/data/anki-note-data-creator.js
··· 29 29 export function createAnkiNoteData(marker, { 30 30 dictionaryEntry, 31 31 resultOutputMode, 32 - mode, 33 32 glossaryLayoutMode, 34 33 compactTags, 35 34 context, ··· 63 62 compactTags, 64 63 group: (resultOutputMode === 'group'), 65 64 merge: (resultOutputMode === 'merge'), 66 - modeTermKanji: (mode === 'term-kanji'), 67 - modeTermKana: (mode === 'term-kana'), 68 - modeKanji: (mode === 'kanji'), 69 65 compactGlossaries: (glossaryLayoutMode === 'compact'), 70 66 get uniqueExpressions() { return getCachedValue(uniqueExpressions); }, 71 67 get uniqueReadings() { return getCachedValue(uniqueReadings); },
+88
ext/js/data/options-util.js
··· 575 575 this._updateVersion61, 576 576 this._updateVersion62, 577 577 this._updateVersion63, 578 + this._updateVersion64, 578 579 ]; 579 580 /* eslint-enable @typescript-eslint/unbound-method */ 580 581 if (typeof targetVersion === 'number' && targetVersion < result.length) { ··· 1633 1634 */ 1634 1635 async _updateVersion63(options) { 1635 1636 await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v63.handlebars'); 1637 + } 1638 + 1639 + /** 1640 + * - Added multiple anki card formats 1641 + * - Updated expression template to remove modeTermKana 1642 + * - Updated hotkeys to use generic note actions 1643 + * @type {import('options-util').UpdateFunction} 1644 + */ 1645 + async _updateVersion64(options) { 1646 + await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v64.handlebars'); 1647 + 1648 + for (const profile of options.profiles) { 1649 + const oldTerms = profile.options.anki.terms; 1650 + 1651 + const updatedCardFormats = [{ 1652 + name: 'Expression', 1653 + icon: 'big-circle', 1654 + deck: oldTerms.deck, 1655 + model: oldTerms.model, 1656 + fields: oldTerms.fields, 1657 + type: 'term', 1658 + }]; 1659 + 1660 + if (Object.values(oldTerms.fields).some((field) => field.value.includes('{expression}'))) { 1661 + updatedCardFormats.push({ 1662 + name: 'Reading', 1663 + icon: 'small-circle', 1664 + deck: oldTerms.deck, 1665 + model: oldTerms.model, 1666 + fields: Object.fromEntries( 1667 + Object.entries(oldTerms.fields).map(([key, field]) => [ 1668 + key, 1669 + {...field, value: field.value.replace(/{expression}/g, '{reading}')}, 1670 + ]), 1671 + ), 1672 + type: 'term', 1673 + }); 1674 + } 1675 + 1676 + const language = profile.options.general.language; 1677 + const logographLanguages = ['ja', 'zh', 'yue']; 1678 + if (logographLanguages.includes(language)) { 1679 + const oldKanji = profile.options.anki.kanji; 1680 + const kanjiNote = { 1681 + name: language === 'ja' ? 'Kanji' : 'Hanzi', 1682 + icon: 'big-circle', 1683 + deck: oldKanji.deck, 1684 + model: oldKanji.model, 1685 + fields: oldKanji.fields, 1686 + type: 'kanji', 1687 + }; 1688 + updatedCardFormats.push(kanjiNote); 1689 + } 1690 + 1691 + profile.options.anki.cardFormats = [...updatedCardFormats]; 1692 + 1693 + delete profile.options.anki.terms; 1694 + delete profile.options.anki.kanji; 1695 + 1696 + if (!profile.options.inputs || !profile.options.inputs.hotkeys) { 1697 + continue; 1698 + } 1699 + 1700 + for (const hotkey of profile.options.inputs.hotkeys) { 1701 + if (!('argument' in hotkey)) { 1702 + hotkey.argument = ''; 1703 + } 1704 + switch (hotkey.action) { 1705 + case 'addNoteTermKanji': 1706 + hotkey.action = 'addNote'; 1707 + hotkey.argument = '0'; 1708 + break; 1709 + case 'addNoteTermKana': 1710 + hotkey.action = 'addNote'; 1711 + hotkey.argument = `${Math.min(1, updatedCardFormats.length - 1)}`; 1712 + break; 1713 + case 'addNoteKanji': 1714 + hotkey.action = 'addNote'; 1715 + hotkey.argument = `${updatedCardFormats.length - 1}`; 1716 + break; 1717 + case 'viewNotes': 1718 + hotkey.action = 'viewNotes'; 1719 + hotkey.argument = '0'; 1720 + break; 1721 + } 1722 + } 1723 + } 1636 1724 } 1637 1725 1638 1726 /**
+2 -4
ext/js/data/permissions-util.js
··· 129 129 if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) { 130 130 return false; 131 131 } 132 - const fieldsList = [ 133 - options.anki.terms.fields, 134 - options.anki.kanji.fields, 135 - ]; 132 + const fieldsList = options.anki.cardFormats.map((cardFormat) => cardFormat.fields); 133 + 136 134 for (const fields of fieldsList) { 137 135 for (const {value: fieldValue} of Object.values(fields)) { 138 136 const markers = getFieldMarkers(fieldValue);
+287 -237
ext/js/display/display-anki.js
··· 91 91 this._audioDownloadIdleTimeout = null; 92 92 /** @type {string[]} */ 93 93 this._noteTags = []; 94 - /** @type {Map<import('display-anki').CreateMode, import('settings').AnkiNoteOptions>} */ 95 - this._modeOptions = new Map(); 94 + /** @type {import('settings').AnkiCardFormat[]} */ 95 + this._cardFormats = []; 96 96 /** @type {import('settings').DictionariesOptions} */ 97 97 this._dictionaries = []; 98 - /** @type {Map<import('dictionary').DictionaryEntryType, import('display-anki').CreateMode[]>} */ 99 - this._dictionaryEntryTypeModeMap = new Map([ 100 - ['kanji', ['kanji']], 101 - ['term', ['term-kanji', 'term-kana']], 102 - ]); 103 98 /** @type {HTMLElement} */ 104 99 this._menuContainer = querySelectorNotNull(document, '#popup-menus'); 105 100 /** @type {(event: MouseEvent) => void} */ ··· 121 116 this._noteContext = this._getNoteContext(); 122 117 /* eslint-disable @stylistic/no-multi-spaces */ 123 118 this._display.hotkeyHandler.registerActions([ 124 - ['addNoteKanji', () => { this._hotkeySaveAnkiNoteForSelectedEntry('kanji'); }], 125 - ['addNoteTermKanji', () => { this._hotkeySaveAnkiNoteForSelectedEntry('term-kanji'); }], 126 - ['addNoteTermKana', () => { this._hotkeySaveAnkiNoteForSelectedEntry('term-kana'); }], 127 - ['viewNotes', this._viewNotesForSelectedEntry.bind(this)], 119 + ['addNote', this._hotkeySaveAnkiNoteForSelectedEntry.bind(this)], 120 + ['viewNotes', this._hotkeyViewNotesForSelectedEntry.bind(this)], 128 121 ]); 129 122 /* eslint-enable @stylistic/no-multi-spaces */ 130 123 this._display.on('optionsUpdated', this._onOptionsUpdated.bind(this)); 131 124 this._display.on('contentClear', this._onContentClear.bind(this)); 132 125 this._display.on('contentUpdateStart', this._onContentUpdateStart.bind(this)); 133 - this._display.on('contentUpdateEntry', this._onContentUpdateEntry.bind(this)); 134 126 this._display.on('contentUpdateComplete', this._onContentUpdateComplete.bind(this)); 135 127 this._display.on('logDictionaryEntryData', this._onLogDictionaryEntryData.bind(this)); 136 128 } ··· 147 139 if (this._noteContext === null) { throw new Error('Note context not initialized'); } 148 140 ankiNoteData = await this._ankiNoteBuilder.getRenderingData({ 149 141 dictionaryEntry, 150 - mode: 'test', 142 + cardFormat: this._cardFormats[0], 151 143 context: this._noteContext, 152 144 resultOutputMode: this._resultOutputMode, 153 145 glossaryLayoutMode: this._glossaryLayoutMode, ··· 162 154 // Anki notes 163 155 /** @type {import('display-anki').AnkiNoteLogData[]} */ 164 156 const ankiNotes = []; 165 - const modes = this._getModes(dictionaryEntry.type === 'term'); 166 - for (const mode of modes) { 157 + for (const [cardFormatIndex] of this._cardFormats.entries()) { 167 158 let note; 168 159 let errors; 169 160 let requirements; 170 161 try { 171 - ({note: note, errors, requirements} = await this._createNote(dictionaryEntry, mode, [])); 162 + ({note: note, errors, requirements} = await this._createNote(dictionaryEntry, cardFormatIndex, [])); 172 163 } catch (e) { 173 164 errors = [toError(e)]; 174 165 } 175 166 /** @type {import('display-anki').AnkiNoteLogData} */ 176 - const entry = {mode, note}; 167 + const entry = {cardFormatIndex, note}; 177 168 if (Array.isArray(errors) && errors.length > 0) { 178 169 entry.errors = errors; 179 170 } ··· 211 202 suspendNewCards, 212 203 checkForDuplicates, 213 204 displayTagsAndFlags, 214 - kanji, 215 - terms, 205 + cardFormats, 216 206 noteGuiMode, 217 207 screenshot: {format, quality}, 218 208 downloadTimeout, ··· 235 225 this._noteGuiMode = noteGuiMode; 236 226 this._noteTags = [...tags]; 237 227 this._audioDownloadIdleTimeout = (Number.isFinite(downloadTimeout) && downloadTimeout > 0 ? downloadTimeout : null); 238 - this._modeOptions.clear(); 239 - this._modeOptions.set('kanji', kanji); 240 - this._modeOptions.set('term-kanji', terms); 241 - this._modeOptions.set('term-kana', terms); 228 + this._cardFormats = cardFormats; 242 229 this._dictionaries = dictionaries; 243 230 244 231 void this._updateAnkiFieldTemplates(options); ··· 257 244 this._noteContext = this._getNoteContext(); 258 245 } 259 246 260 - /** 261 - * @param {import('display').EventArgument<'contentUpdateEntry'>} details 262 - */ 263 - _onContentUpdateEntry({element}) { 264 - const eventListeners = this._eventListeners; 265 - for (const node of element.querySelectorAll('.action-button[data-action=view-tags]')) { 266 - eventListeners.addEventListener(node, 'click', this._onShowTagsBind); 267 - } 268 - for (const node of element.querySelectorAll('.action-button[data-action=view-flags]')) { 269 - eventListeners.addEventListener(node, 'click', this._onShowFlagsBind); 270 - } 271 - for (const node of element.querySelectorAll('.action-button[data-action=save-note]')) { 272 - eventListeners.addEventListener(node, 'click', this._onNoteSaveBind); 273 - } 274 - for (const node of element.querySelectorAll('.action-button[data-action=view-note]')) { 275 - eventListeners.addEventListener(node, 'click', this._onViewNotesButtonClickBind); 276 - eventListeners.addEventListener(node, 'contextmenu', this._onViewNotesButtonContextMenuBind); 277 - eventListeners.addEventListener(node, 'menuClose', this._onViewNotesButtonMenuCloseBind); 278 - } 279 - } 280 - 281 247 /** */ 282 248 _onContentUpdateComplete() { 283 249 void this._updateDictionaryEntryDetails(); ··· 292 258 293 259 /** 294 260 * @param {MouseEvent} e 261 + * @throws {Error} 295 262 */ 296 263 _onNoteSave(e) { 297 264 e.preventDefault(); 298 265 const element = /** @type {HTMLElement} */ (e.currentTarget); 299 - const mode = this._getValidCreateMode(element.dataset.mode); 300 - if (mode === null) { return; } 266 + const cardFormatIndex = element.dataset.cardFormatIndex; 267 + if (!cardFormatIndex || !Number.isInteger(Number.parseInt(cardFormatIndex, 10))) { 268 + throw new Error(`Invalid note options index: ${cardFormatIndex}`); 269 + } 301 270 const index = this._display.getElementDictionaryEntryIndex(element); 302 - void this._saveAnkiNote(index, mode); 271 + void this._saveAnkiNote(index, Number.parseInt(cardFormatIndex, 10)); 303 272 } 304 273 305 274 /** ··· 324 293 325 294 /** 326 295 * @param {number} index 327 - * @param {import('display-anki').CreateMode} mode 296 + * @param {number} cardFormatIndex 328 297 * @returns {?HTMLButtonElement} 329 298 */ 330 - _saveButtonFind(index, mode) { 299 + _createSaveButtons(index, cardFormatIndex) { 331 300 const entry = this._getEntry(index); 332 - return entry !== null ? entry.querySelector(`.action-button[data-action=save-note][data-mode="${mode}"]`) : null; 333 - } 301 + if (entry === null) { return null; } 302 + 303 + const container = entry.querySelector('.note-actions-container'); 304 + if (container === null) { return null; } 305 + 306 + // Create button from template 307 + const singleNoteActionButtons = /** @type {HTMLElement} */ (this._display.displayGenerator.instantiateTemplate('action-button-container')); 308 + /** @type {HTMLButtonElement} */ 309 + const saveButton = querySelectorNotNull(singleNoteActionButtons, '.action-button'); 310 + /** @type {HTMLElement} */ 311 + const iconSpan = querySelectorNotNull(saveButton, '.action-icon'); 312 + // Set button properties 313 + const cardFormat = this._cardFormats[cardFormatIndex]; 314 + singleNoteActionButtons.dataset.cardFormatIndex = cardFormatIndex.toString(); 315 + saveButton.title = `Add ${cardFormat.name} note`; 316 + saveButton.dataset.cardFormatIndex = cardFormatIndex.toString(); 317 + iconSpan.dataset.icon = cardFormat.icon; 318 + 319 + const saveButtonIndex = container.children.length; 320 + if ([0, 1].includes(saveButtonIndex)) { 321 + saveButton.dataset.hotkey = `["addNote${saveButtonIndex + 1}","title","Add ${cardFormat.name} note"]`; 322 + // eslint-disable-next-line no-underscore-dangle 323 + this._display._hotkeyHelpController.setHotkeyLabel(saveButton, `Add ${cardFormat.name} note ({0})`); 324 + } else { 325 + delete saveButton.dataset.hotkey; 326 + } 327 + // Add event listeners 328 + this._eventListeners.addEventListener(saveButton, 'click', this._onNoteSaveBind); 329 + 330 + // Add button to container 331 + container.appendChild(singleNoteActionButtons); 334 332 335 - /** 336 - * @param {number} index 337 - * @returns {?HTMLButtonElement} 338 - */ 339 - _tagsIndicatorFind(index) { 340 - const entry = this._getEntry(index); 341 - return entry !== null ? entry.querySelector('.action-button[data-action=view-tags]') : null; 333 + return saveButton; 342 334 } 343 335 344 - /** 345 - * @param {number} index 346 - * @returns {?HTMLButtonElement} 347 - */ 348 - _flagsIndicatorFind(index) { 349 - const entry = this._getEntry(index); 350 - return entry !== null ? entry.querySelector('.action-button[data-action=view-flags]') : null; 351 - } 352 336 353 337 /** 354 338 * @param {number} index ··· 404 388 if (this._updateDictionaryEntryDetailsToken !== token) { return; } 405 389 this._dictionaryEntryDetails = dictionaryEntryDetails; 406 390 this._updateSaveButtons(dictionaryEntryDetails); 391 + // eslint-disable-next-line no-underscore-dangle 392 + this._display._hotkeyHelpController.setupNode(document.documentElement); 407 393 } finally { 408 394 resolve(); 409 395 if (this._updateSaveButtonsPromise === promise) { ··· 415 401 /** 416 402 * @param {HTMLButtonElement} button 417 403 * @param {number[]} noteIds 404 + * @throws {Error} 418 405 */ 419 406 _updateSaveButtonForDuplicateBehavior(button, noteIds) { 420 407 const behavior = this._duplicateBehavior; ··· 423 410 return; 424 411 } 425 412 426 - const mode = button.dataset.mode; 413 + const cardFormatIndex = button.dataset.cardFormatIndex; 414 + if (typeof cardFormatIndex === 'undefined') { throw new Error('Invalid note options index'); } 415 + const cardFormatIndexNumber = Number.parseInt(cardFormatIndex, 10); 416 + if (Number.isNaN(cardFormatIndexNumber)) { throw new Error('Invalid note options index'); } 417 + const cardFormat = this._cardFormats[cardFormatIndexNumber]; 418 + 427 419 const verb = behavior === 'overwrite' ? 'Overwrite' : 'Add duplicate'; 428 420 const iconPrefix = behavior === 'overwrite' ? 'overwrite' : 'add-duplicate'; 429 - const target = mode === 'term-kanji' ? 'expression' : 'reading'; 421 + const target = `${cardFormat.name} note`; 430 422 431 423 if (behavior === 'overwrite') { 432 424 button.dataset.overwrite = 'true'; ··· 437 429 delete button.dataset.overwrite; 438 430 } 439 431 440 - button.setAttribute('title', `${verb} ${target}`); 432 + const title = `${verb} ${target}`; 433 + button.setAttribute('title', title); 441 434 442 435 // eslint-disable-next-line no-underscore-dangle 443 436 const hotkeyLabel = this._display._hotkeyHelpController.getHotkeyLabel(button); 444 437 if (hotkeyLabel) { 445 438 // eslint-disable-next-line no-underscore-dangle 446 - this._display._hotkeyHelpController.setHotkeyLabel(button, `${verb} ${target} ({0})`); 439 + this._display._hotkeyHelpController.setHotkeyLabel(button, `${title} ({0})`); // {0} is a placeholder that gets replaced with the actual hotkey combination. For example, "Add expression (Ctrl+1)" or "Overwrite reading (Ctrl+2)" 447 440 } 448 441 449 442 const actionIcon = button.querySelector('.action-icon'); 450 443 if (actionIcon instanceof HTMLElement) { 451 - actionIcon.dataset.icon = `${iconPrefix}-${mode}`; 444 + actionIcon.dataset.icon = `${iconPrefix}-${cardFormat.icon}`; 452 445 } 453 446 } 454 447 ··· 457 450 */ 458 451 _updateSaveButtons(dictionaryEntryDetails) { 459 452 const displayTagsAndFlags = this._displayTagsAndFlags; 460 - for (let i = 0, ii = dictionaryEntryDetails.length; i < ii; ++i) { 461 - /** @type {?Set<number>} */ 462 - let allNoteIds = null; 463 - for (const {mode, canAdd, noteIds, noteInfos, ankiError} of dictionaryEntryDetails[i].modeMap.values()) { 464 - const button = this._saveButtonFind(i, mode); 453 + for (let entryIndex = 0, entryCount = dictionaryEntryDetails.length; entryIndex < entryCount; ++entryIndex) { 454 + for (const [cardFormatIndex, {canAdd, noteIds, noteInfos, ankiError}] of dictionaryEntryDetails[entryIndex].noteMap.entries()) { 455 + const button = this._createSaveButtons(entryIndex, cardFormatIndex); 465 456 if (button !== null) { 466 457 button.disabled = !canAdd; 467 458 button.hidden = (ankiError !== null); ··· 475 466 } 476 467 } 477 468 478 - if (Array.isArray(noteIds) && noteIds.length > 0) { 479 - if (allNoteIds === null) { allNoteIds = new Set(); } 480 - for (const noteId of noteIds) { 481 - if (noteId !== INVALID_NOTE_ID) { 482 - allNoteIds.add(noteId); 483 - } 484 - } 485 - } 469 + const validNoteIds = noteIds?.filter((id) => id !== INVALID_NOTE_ID) ?? []; 470 + 471 + this._createViewNoteButton(entryIndex, cardFormatIndex, validNoteIds); 486 472 487 473 if (displayTagsAndFlags !== 'never' && Array.isArray(noteInfos)) { 488 - this._setupTagsIndicator(i, noteInfos); 489 - this._setupFlagsIndicator(i, noteInfos); 474 + this._setupTagsIndicator(entryIndex, cardFormatIndex, noteInfos); 475 + this._setupFlagsIndicator(entryIndex, cardFormatIndex, noteInfos); 490 476 } 491 477 } 492 - 493 - this._updateViewNoteButton(i, allNoteIds !== null ? [...allNoteIds] : [], false); 494 478 } 495 479 } 496 480 497 481 /** 498 482 * @param {number} i 483 + * @param {number} cardFormatIndex 499 484 * @param {(?import('anki').NoteInfo)[]} noteInfos 500 485 */ 501 - _setupTagsIndicator(i, noteInfos) { 502 - const tagsIndicator = this._tagsIndicatorFind(i); 503 - if (tagsIndicator === null) { 504 - return; 505 - } 486 + _setupTagsIndicator(i, cardFormatIndex, noteInfos) { 487 + const entry = this._getEntry(i); 488 + if (entry === null) { return; } 489 + 490 + const container = entry.querySelector(`[data-card-format-index="${cardFormatIndex}"]`); 491 + if (container === null) { return; } 492 + 493 + const tagsIndicator = /** @type {HTMLButtonElement} */ (this._display.displayGenerator.instantiateTemplate('note-action-button-view-tags')); 494 + if (tagsIndicator === null) { return; } 506 495 507 496 const displayTags = new Set(); 508 497 for (const item of noteInfos) { ··· 521 510 tagsIndicator.disabled = false; 522 511 tagsIndicator.hidden = false; 523 512 tagsIndicator.title = `Card tags: ${[...displayTags].join(', ')}`; 513 + tagsIndicator.addEventListener('click', this._onShowTagsBind); 514 + container.appendChild(tagsIndicator); 524 515 } 525 516 } 526 517 527 518 /** 528 - * @param {string} message 519 + * @param {number} i 520 + * @param {number} cardFormatIndex 521 + * @param {(?import('anki').NoteInfo)[]} noteInfos 529 522 */ 530 - _showTagsNotification(message) { 531 - if (this._tagsNotification === null) { 532 - this._tagsNotification = this._display.createNotification(true); 533 - } 523 + _setupFlagsIndicator(i, cardFormatIndex, noteInfos) { 524 + const entry = this._getEntry(i); 525 + if (entry === null) { return; } 534 526 535 - this._tagsNotification.setContent(message); 536 - this._tagsNotification.open(); 537 - } 527 + const container = entry.querySelector(`[data-card-format-index="${cardFormatIndex}"]`); 528 + if (container === null) { return; } 538 529 539 - /** 540 - * @param {number} i 541 - * @param {(?import('anki').NoteInfo)[]} noteInfos 542 - */ 543 - _setupFlagsIndicator(i, noteInfos) { 544 - const flagsIndicator = this._flagsIndicatorFind(i); 545 - if (flagsIndicator === null) { 546 - return; 547 - } 530 + const flagsIndicator = /** @type {HTMLButtonElement} */ (this._display.displayGenerator.instantiateTemplate('note-action-button-view-flags')); 531 + if (flagsIndicator === null) { return; } 548 532 549 533 /** @type {Set<string>} */ 550 534 const displayFlags = new Set(); ··· 566 550 if (flagsIndicatorIcon !== null && flagsIndicator instanceof HTMLElement) { 567 551 flagsIndicatorIcon.style.background = this._getFlagColor(displayFlags); 568 552 } 553 + flagsIndicator.addEventListener('click', this._onShowFlagsBind); 554 + container.appendChild(flagsIndicator); 569 555 } 570 556 } 571 557 572 558 /** 573 - * @param {number} flag 574 - * @returns {string} 559 + * @param {string} message 575 560 */ 576 - _getFlagName(flag) { 577 - /** @type {Record<number, string>} */ 578 - const flagNamesDict = { 579 - 1: 'Red', 580 - 2: 'Orange', 581 - 3: 'Green', 582 - 4: 'Blue', 583 - 5: 'Pink', 584 - 6: 'Turquoise', 585 - 7: 'Purple', 586 - }; 587 - if (flag in flagNamesDict) { 588 - return flagNamesDict[flag]; 561 + _showTagsNotification(message) { 562 + if (this._tagsNotification === null) { 563 + this._tagsNotification = this._display.createNotification(true); 589 564 } 590 - return ''; 591 - } 592 565 593 - /** 594 - * @param {Set<string>} flags 595 - * @returns {string} 596 - */ 597 - _getFlagColor(flags) { 598 - /** @type {Record<string, import('display-anki').RGB>} */ 599 - const flagColorsDict = { 600 - Red: {red: 248, green: 113, blue: 113}, 601 - Orange: {red: 253, green: 186, blue: 116}, 602 - Green: {red: 134, green: 239, blue: 172}, 603 - Blue: {red: 96, green: 165, blue: 250}, 604 - Pink: {red: 240, green: 171, blue: 252}, 605 - Turquoise: {red: 94, green: 234, blue: 212}, 606 - Purple: {red: 192, green: 132, blue: 252}, 607 - }; 608 - 609 - const gradientSliceSize = 100 / flags.size; 610 - let currentGradientPercent = 0; 611 - 612 - const gradientSlices = []; 613 - for (const flag of flags) { 614 - const flagColor = flagColorsDict[flag]; 615 - gradientSlices.push( 616 - 'rgb(' + flagColor.red + ',' + flagColor.green + ',' + flagColor.blue + ') ' + currentGradientPercent + '%', 617 - 'rgb(' + flagColor.red + ',' + flagColor.green + ',' + flagColor.blue + ') ' + (currentGradientPercent + gradientSliceSize) + '%', 618 - ); 619 - currentGradientPercent += gradientSliceSize; 620 - } 621 - 622 - return 'linear-gradient(to right,' + gradientSlices.join(',') + ')'; 566 + this._tagsNotification.setContent(message); 567 + this._tagsNotification.open(); 623 568 } 624 569 625 570 /** ··· 635 580 } 636 581 637 582 /** 638 - * @param {import('display-anki').CreateMode} mode 583 + * @param {unknown} cardFormatStringIndex 639 584 */ 640 - _hotkeySaveAnkiNoteForSelectedEntry(mode) { 585 + _hotkeySaveAnkiNoteForSelectedEntry(cardFormatStringIndex) { 586 + if (typeof cardFormatStringIndex !== 'string') { return; } 587 + const cardFormatIndex = Number.parseInt(cardFormatStringIndex, 10); 588 + if (Number.isNaN(cardFormatIndex)) { return; } 641 589 const index = this._display.selectedIndex; 642 - void this._saveAnkiNote(index, mode); 590 + const entry = this._getEntry(index); 591 + if (entry === null) { return; } 592 + const container = entry.querySelector('.note-actions-container'); 593 + if (container === null) { return; } 594 + /** @type {HTMLButtonElement | null} */ 595 + const nthButton = container.querySelector(`.action-button[data-action=save-note][data-card-format-index="${cardFormatIndex}"]`); 596 + if (nthButton === null) { return; } 597 + void this._saveAnkiNote(index, cardFormatIndex); 643 598 } 644 599 645 600 /** 646 601 * @param {number} dictionaryEntryIndex 647 - * @param {import('display-anki').CreateMode} mode 602 + * @param {number} cardFormatIndex 648 603 */ 649 - async _saveAnkiNote(dictionaryEntryIndex, mode) { 604 + async _saveAnkiNote(dictionaryEntryIndex, cardFormatIndex) { 650 605 const dictionaryEntries = this._display.dictionaryEntries; 651 606 const dictionaryEntryDetails = this._dictionaryEntryDetails; 652 607 if (!( ··· 658 613 return; 659 614 } 660 615 const dictionaryEntry = dictionaryEntries[dictionaryEntryIndex]; 661 - const details = dictionaryEntryDetails[dictionaryEntryIndex].modeMap.get(mode); 616 + const details = dictionaryEntryDetails[dictionaryEntryIndex].noteMap.get(cardFormatIndex); 662 617 if (typeof details === 'undefined') { return; } 663 618 664 619 const {requirements} = details; 665 620 666 - const button = this._saveButtonFind(dictionaryEntryIndex, mode); 621 + const button = this._saveButtonFind(dictionaryEntryIndex, cardFormatIndex); 667 622 if (button === null || button.disabled) { return; } 668 623 669 624 this._hideErrorNotification(true); ··· 673 628 const progressIndicatorVisible = this._display.progressIndicatorVisible; 674 629 const overrideToken = progressIndicatorVisible.setOverride(true); 675 630 try { 676 - const {note, errors, requirements: outputRequirements} = await this._createNote(dictionaryEntry, mode, requirements); 631 + const {note, errors, requirements: outputRequirements} = await this._createNote(dictionaryEntry, cardFormatIndex, requirements); 677 632 allErrors.push(...errors); 678 633 679 634 const error = this._getAddNoteRequirementsError(requirements, outputRequirements); 680 635 if (error !== null) { allErrors.push(error); } 681 636 if (button.dataset.overwrite) { 682 - const overwrittenNote = await this._getOverwrittenNote(note, dictionaryEntryIndex, mode); 637 + const overwrittenNote = await this._getOverwrittenNote(note, dictionaryEntryIndex, cardFormatIndex); 683 638 await this._updateAnkiNote(overwrittenNote, allErrors); 684 639 } else { 685 640 await this._addNewAnkiNote(note, allErrors, button, dictionaryEntryIndex); ··· 698 653 } 699 654 700 655 /** 656 + * @param {number} dictionaryEntryIndex 657 + * @param {number} cardFormatIndex 658 + * @returns {?HTMLButtonElement} 659 + */ 660 + _saveButtonFind(dictionaryEntryIndex, cardFormatIndex) { 661 + const entry = this._getEntry(dictionaryEntryIndex); 662 + if (entry === null) { return null; } 663 + const container = entry.querySelector('.note-actions-container'); 664 + if (container === null) { return null; } 665 + const singleNoteActionButtonContainer = container.querySelector(`[data-card-format-index="${cardFormatIndex}"]`); 666 + if (singleNoteActionButtonContainer === null) { return null; } 667 + return singleNoteActionButtonContainer.querySelector('.action-button[data-action=save-note]'); 668 + } 669 + 670 + /** 701 671 * @param {import('anki').Note} note 702 672 * @param {number} dictionaryEntryIndex 703 - * @param {import('display-anki').CreateMode} mode 673 + * @param {number} cardFormatIndex 704 674 * @returns {Promise<import('anki').NoteWithId | null>} 705 675 */ 706 - async _getOverwrittenNote(note, dictionaryEntryIndex, mode) { 676 + async _getOverwrittenNote(note, dictionaryEntryIndex, cardFormatIndex) { 707 677 const dictionaryEntries = this._display.dictionaryEntries; 708 678 const allEntryDetails = await this._getDictionaryEntryDetails(dictionaryEntries); 709 679 const relevantEntryDetails = allEntryDetails[dictionaryEntryIndex]; 710 - const relevantModeDetails = relevantEntryDetails.modeMap.get(mode); 711 - if (typeof relevantModeDetails === 'undefined') { return null; } 712 - const {noteIds, noteInfos} = relevantModeDetails; 680 + const relevantNoteDetails = relevantEntryDetails.noteMap.get(cardFormatIndex); 681 + if (typeof relevantNoteDetails === 'undefined') { return null; } 682 + const {noteIds, noteInfos} = relevantNoteDetails; 713 683 if (noteIds === null || typeof noteInfos === 'undefined') { return null; } 714 684 const overwriteId = noteIds.find((id) => id !== INVALID_NOTE_ID); 715 685 if (typeof overwriteId === 'undefined') { return null; } 716 686 const overwriteInfo = noteInfos.find((info) => info !== null && info.noteId === overwriteId); 717 687 if (!overwriteInfo) { return null; } 718 688 const existingFields = overwriteInfo.fields; 719 - const fieldOptions = this._modeOptions.get(mode)?.fields; 689 + const fieldOptions = this._cardFormats[cardFormatIndex].fields; 720 690 if (!fieldOptions) { return null; } 721 691 722 692 const newValues = note.fields; ··· 786 756 allErrors.push(toError(e)); 787 757 } 788 758 } 759 + const cardFormatIndex = this._getCardFormatIndex(button); 760 + 789 761 this._updateSaveButtonForDuplicateBehavior(button, [noteId]); 790 762 791 - this._updateViewNoteButton(dictionaryEntryIndex, [noteId], true); 763 + this._updateViewNoteButton(dictionaryEntryIndex, cardFormatIndex, [noteId]); 792 764 } 793 765 } 794 766 } 795 767 796 768 /** 769 + * @param {HTMLButtonElement} button 770 + * @returns {number} 771 + * @throws {Error} 772 + */ 773 + _getCardFormatIndex(button) { 774 + const cardFormatIndex = button.dataset.cardFormatIndex; 775 + if (typeof cardFormatIndex === 'undefined') { throw new Error('Invalid card format index'); } 776 + const cardFormatIndexNumber = Number.parseInt(cardFormatIndex, 10); 777 + if (Number.isNaN(cardFormatIndexNumber)) { throw new Error('Invalid card format index'); } 778 + return cardFormatIndexNumber; 779 + } 780 + 781 + /** 782 + * @param {number} dictionaryEntryIndex 783 + * @param {number} cardFormatIndex 784 + * @param {number[]} noteIds 785 + */ 786 + _updateViewNoteButton(dictionaryEntryIndex, cardFormatIndex, noteIds) { 787 + const entry = this._getEntry(dictionaryEntryIndex); 788 + if (entry === null) { return; } 789 + const singleNoteActions = entry.querySelector(`[data-card-format-index="${cardFormatIndex}"]`); 790 + if (singleNoteActions === null) { return; } 791 + /** @type {HTMLButtonElement | null} */ 792 + let viewNoteButton = singleNoteActions.querySelector('.action-button[data-action=view-note]'); 793 + if (viewNoteButton === null) { 794 + viewNoteButton = this._createViewNoteButton(dictionaryEntryIndex, cardFormatIndex, noteIds); 795 + } 796 + if (viewNoteButton === null) { return; } 797 + const newNoteIds = new Set([...this._getNodeNoteIds(viewNoteButton), ...noteIds]); 798 + viewNoteButton.dataset.noteIds = [...newNoteIds].join(' '); 799 + this._setViewButtonBadge(viewNoteButton); 800 + viewNoteButton.hidden = false; 801 + } 802 + 803 + /** 797 804 * @param {import('anki').NoteWithId | null} noteWithId 798 805 * @param {Error[]} allErrors 799 806 */ ··· 918 925 for (let i = 0, ii = dictionaryEntries.length; i < ii; ++i) { 919 926 const dictionaryEntry = dictionaryEntries[i]; 920 927 const {type} = dictionaryEntry; 921 - const modes = this._dictionaryEntryTypeModeMap.get(type); 922 - if (typeof modes === 'undefined') { continue; } 923 - for (const mode of modes) { 924 - const notePromise = this._createNote(dictionaryEntry, mode, []); 928 + for (const [cardFormatIndex, cardFormat] of this._cardFormats.entries()) { 929 + if (cardFormat.type !== type) { continue; } 930 + const notePromise = this._createNote(dictionaryEntry, cardFormatIndex, []); 925 931 notePromises.push(notePromise); 926 - noteTargets.push({index: i, mode}); 932 + noteTargets.push({index: i, cardFormatIndex, cardFormat}); 927 933 } 928 934 } 929 935 ··· 946 952 } 947 953 948 954 /** @type {import('display-anki').DictionaryEntryDetails[]} */ 949 - const results = []; 950 - for (let i = 0, ii = dictionaryEntries.length; i < ii; ++i) { 951 - results.push({ 952 - modeMap: new Map(), 953 - }); 954 - } 955 + const results = new Array(dictionaryEntries.length).fill(null).map(() => ({noteMap: new Map()})); 955 956 956 957 for (let i = 0, ii = noteInfoList.length; i < ii; ++i) { 957 958 const {note, errors, requirements} = noteInfoList[i]; 958 959 const {canAdd, valid, noteIds, noteInfos} = infos[i]; 959 - const {mode, index} = noteTargets[i]; 960 - results[index].modeMap.set(mode, {mode, note, errors, requirements, canAdd, valid, noteIds, noteInfos, ankiError}); 960 + const {cardFormatIndex, cardFormat, index} = noteTargets[i]; 961 + results[index].noteMap.set(cardFormatIndex, {cardFormat, note, errors, requirements, canAdd, valid, noteIds, noteInfos, ankiError}); 961 962 } 962 963 return results; 963 964 } ··· 978 979 979 980 /** 980 981 * @param {import('dictionary').DictionaryEntry} dictionaryEntry 981 - * @param {import('display-anki').CreateMode} mode 982 + * @param {number} cardFormatIndex 982 983 * @param {import('anki-note-builder').Requirement[]} requirements 983 984 * @returns {Promise<import('display-anki').CreateNoteResult>} 984 985 */ 985 - async _createNote(dictionaryEntry, mode, requirements) { 986 + async _createNote(dictionaryEntry, cardFormatIndex, requirements) { 986 987 const context = this._noteContext; 987 988 if (context === null) { throw new Error('Note context not initialized'); } 988 - const modeOptions = this._modeOptions.get(mode); 989 - if (typeof modeOptions === 'undefined') { throw new Error(`Unsupported note type: ${mode}`); } 989 + const cardFormat = this._cardFormats?.[cardFormatIndex]; 990 + if (typeof cardFormat === 'undefined') { throw new Error('Unsupported note type}'); } 990 991 if (!this._ankiFieldTemplates) { 991 992 const options = this._display.getOptions(); 992 993 if (options) { ··· 995 996 } 996 997 const template = this._ankiFieldTemplates; 997 998 if (typeof template !== 'string') { throw new Error('Invalid template'); } 998 - const {deck: deckName, model: modelName} = modeOptions; 999 - const fields = Object.entries(modeOptions.fields); 1000 999 const contentOrigin = this._display.getContentOrigin(); 1001 1000 const details = this._ankiNoteBuilder.getDictionaryEntryDetailsForNote(dictionaryEntry); 1002 1001 const audioDetails = this._getAnkiNoteMediaAudioDetails(details); ··· 1005 1004 1006 1005 const {note, errors, requirements: outputRequirements} = await this._ankiNoteBuilder.createNote({ 1007 1006 dictionaryEntry, 1008 - mode, 1007 + cardFormat, 1009 1008 context, 1010 1009 template, 1011 - deckName, 1012 - modelName, 1013 - fields, 1014 1010 tags: this._noteTags, 1015 1011 duplicateScope: this._duplicateScope, 1016 1012 duplicateScopeCheckAllModels: this._duplicateScopeCheckAllModels, ··· 1036 1032 } 1037 1033 1038 1034 /** 1039 - * @param {boolean} isTerms 1040 - * @returns {import('display-anki').CreateMode[]} 1041 - */ 1042 - _getModes(isTerms) { 1043 - return isTerms ? ['term-kanji', 'term-kana'] : ['kanji']; 1044 - } 1045 - 1046 - /** 1047 1035 * @param {unknown} sentence 1048 1036 * @param {string} fallback 1049 1037 * @param {number} fallbackOffset ··· 1120 1108 1121 1109 /** 1122 1110 * @param {number} index 1111 + * @param {number} cardFormatIndex 1123 1112 * @param {number[]} noteIds 1124 - * @param {boolean} prepend 1113 + * @returns {?HTMLButtonElement} 1125 1114 */ 1126 - _updateViewNoteButton(index, noteIds, prepend) { 1127 - const button = this._getViewNoteButton(index); 1128 - if (button === null) { return; } 1129 - /** @type {(number|string)[]} */ 1130 - let allNoteIds = noteIds; 1131 - if (prepend) { 1132 - const currentNoteIds = button.dataset.noteIds; 1133 - if (typeof currentNoteIds === 'string' && currentNoteIds.length > 0) { 1134 - allNoteIds = [...allNoteIds, ...currentNoteIds.split(' ')]; 1135 - } 1136 - } 1137 - const disabled = (allNoteIds.length === 0); 1138 - button.disabled = disabled; 1139 - button.hidden = disabled; 1140 - button.dataset.noteIds = allNoteIds.join(' '); 1115 + _createViewNoteButton(index, cardFormatIndex, noteIds) { 1116 + if (noteIds.length === 0) { return null; } 1117 + const viewNoteButton = /** @type {HTMLButtonElement} */ (this._display.displayGenerator.instantiateTemplate('note-action-button-view-note')); 1118 + if (viewNoteButton === null) { return null; } 1119 + const disabled = (noteIds.length === 0); 1120 + viewNoteButton.disabled = disabled; 1121 + viewNoteButton.hidden = disabled; 1122 + viewNoteButton.dataset.noteIds = noteIds.join(' '); 1123 + 1124 + this._setViewButtonBadge(viewNoteButton); 1141 1125 1126 + const entry = this._getEntry(index); 1127 + if (entry === null) { return null; } 1128 + 1129 + const container = entry.querySelector('.note-actions-container'); 1130 + if (container === null) { return null; } 1131 + const singleNoteActionButtonContainer = container.querySelector(`[data-card-format-index="${cardFormatIndex}"]`); 1132 + if (singleNoteActionButtonContainer === null) { return null; } 1133 + singleNoteActionButtonContainer.appendChild(viewNoteButton); 1134 + 1135 + this._eventListeners.addEventListener(viewNoteButton, 'click', this._onViewNotesButtonClickBind); 1136 + this._eventListeners.addEventListener(viewNoteButton, 'contextmenu', this._onViewNotesButtonContextMenuBind); 1137 + this._eventListeners.addEventListener(viewNoteButton, 'menuClose', this._onViewNotesButtonMenuCloseBind); 1138 + 1139 + return viewNoteButton; 1140 + } 1141 + 1142 + /** 1143 + * @param {HTMLButtonElement} viewNoteButton 1144 + */ 1145 + _setViewButtonBadge(viewNoteButton) { 1142 1146 /** @type {?HTMLElement} */ 1143 - const badge = button.querySelector('.action-button-badge'); 1147 + const badge = viewNoteButton.querySelector('.action-button-badge'); 1148 + const noteIds = this._getNodeNoteIds(viewNoteButton); 1144 1149 if (badge !== null) { 1145 1150 const badgeData = badge.dataset; 1146 - if (allNoteIds.length > 1) { 1151 + if (noteIds.length > 1) { 1147 1152 badgeData.icon = 'plus-thick'; 1148 1153 badge.hidden = false; 1149 1154 } else { ··· 1229 1234 } 1230 1235 1231 1236 /** 1232 - * Shows notes for selected pop-up entry when "View Notes" hotkey is used. 1237 + * @param {unknown} cardFormatStringIndex 1233 1238 */ 1234 - _viewNotesForSelectedEntry() { 1239 + _hotkeyViewNotesForSelectedEntry(cardFormatStringIndex) { 1240 + if (typeof cardFormatStringIndex !== 'string') { return; } 1241 + const cardFormatIndex = Number.parseInt(cardFormatStringIndex, 10); 1242 + if (Number.isNaN(cardFormatIndex)) { return; } 1235 1243 const index = this._display.selectedIndex; 1236 - const button = this._getViewNoteButton(index); 1237 - if (button !== null) { 1238 - void this._viewNotes(button); 1244 + const entry = this._getEntry(index); 1245 + if (entry === null) { return; } 1246 + const container = entry.querySelector('.note-actions-container'); 1247 + if (container === null) { return; } 1248 + /** @type {HTMLButtonElement | null} */ 1249 + const nthButton = container.querySelector(`.action-button-container[data-card-format-index="${cardFormatIndex}"] .action-button[data-action=view-note]`); 1250 + if (nthButton === null) { return; } 1251 + void this._viewNotes(nthButton); 1252 + } 1253 + 1254 + /** 1255 + * @param {number} flag 1256 + * @returns {string} 1257 + */ 1258 + _getFlagName(flag) { 1259 + /** @type {Record<number, string>} */ 1260 + const flagNamesDict = { 1261 + 1: 'Red', 1262 + 2: 'Orange', 1263 + 3: 'Green', 1264 + 4: 'Blue', 1265 + 5: 'Pink', 1266 + 6: 'Turquoise', 1267 + 7: 'Purple', 1268 + }; 1269 + if (flag in flagNamesDict) { 1270 + return flagNamesDict[flag]; 1239 1271 } 1272 + return ''; 1240 1273 } 1241 1274 1242 1275 /** 1243 - * @param {string|undefined} value 1244 - * @returns {?import('display-anki').CreateMode} 1276 + * @param {Set<string>} flags 1277 + * @returns {string} 1245 1278 */ 1246 - _getValidCreateMode(value) { 1247 - switch (value) { 1248 - case 'kanji': 1249 - case 'term-kanji': 1250 - case 'term-kana': 1251 - return value; 1252 - default: 1253 - return null; 1279 + _getFlagColor(flags) { 1280 + /** @type {Record<string, import('display-anki').RGB>} */ 1281 + const flagColorsDict = { 1282 + Red: {red: 248, green: 113, blue: 113}, 1283 + Orange: {red: 253, green: 186, blue: 116}, 1284 + Green: {red: 134, green: 239, blue: 172}, 1285 + Blue: {red: 96, green: 165, blue: 250}, 1286 + Pink: {red: 240, green: 171, blue: 252}, 1287 + Turquoise: {red: 94, green: 234, blue: 212}, 1288 + Purple: {red: 192, green: 132, blue: 252}, 1289 + }; 1290 + 1291 + const gradientSliceSize = 100 / flags.size; 1292 + let currentGradientPercent = 0; 1293 + 1294 + const gradientSlices = []; 1295 + for (const flag of flags) { 1296 + const flagColor = flagColorsDict[flag]; 1297 + gradientSlices.push( 1298 + 'rgb(' + flagColor.red + ',' + flagColor.green + ',' + flagColor.blue + ') ' + currentGradientPercent + '%', 1299 + 'rgb(' + flagColor.red + ',' + flagColor.green + ',' + flagColor.blue + ') ' + (currentGradientPercent + gradientSliceSize) + '%', 1300 + ); 1301 + currentGradientPercent += gradientSliceSize; 1254 1302 } 1303 + 1304 + return 'linear-gradient(to right,' + gradientSlices.join(',') + ')'; 1255 1305 } 1256 1306 } 1257 1307
+312 -48
ext/js/pages/settings/anki-controller.js
··· 32 32 /** 33 33 * @param {import('./settings-controller.js').SettingsController} settingsController 34 34 * @param {import('../../application.js').Application} application 35 + * @param {import('./modal-controller.js').ModalController} modalController 35 36 */ 36 - constructor(settingsController, application) { 37 + constructor(settingsController, application, modalController) { 37 38 /** @type {import('../../application.js').Application} */ 38 39 this._application = application; 39 40 /** @type {import('./settings-controller.js').SettingsController} */ 40 41 this._settingsController = settingsController; 42 + /** @type {import('./modal-controller.js').ModalController} */ 43 + this._modalController = modalController; 41 44 /** @type {AnkiConnect} */ 42 45 this._ankiConnect = new AnkiConnect(); 43 46 /** @type {string} */ ··· 73 76 this._duplicateOverwriteWarning = querySelectorNotNull(document, '#anki-overwrite-warning'); 74 77 /** @type {HTMLElement} */ 75 78 this._ankiCardPrimary = querySelectorNotNull(document, '#anki-card-primary'); 79 + /** @type {HTMLElement} */ 80 + this._ankiCardsTabs = querySelectorNotNull(document, '#anki-cards-tabs'); 81 + /** @type {HTMLInputElement} */ 82 + this._ankiCardNameInput = querySelectorNotNull(document, '.anki-card-name'); 83 + /** @type {HTMLInputElement} */ 84 + this._ankiCardDictionaryTypeSelect = querySelectorNotNull(document, '.anki-card-type'); 76 85 /** @type {?Error} */ 77 86 this._ankiError = null; 78 87 /** @type {?import('core').TokenObject} */ 79 88 this._validateFieldsToken = null; 80 89 /** @type {?HTMLInputElement} */ 81 90 this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]'); 91 + /** @type {?import('settings').AnkiOptions} */ 92 + this._ankiOptions = null; 93 + /** @type {number} */ 94 + this._cardFormatIndex = 0; 95 + /** @type {HTMLButtonElement} */ 96 + this._cardFormatDeleteButton = querySelectorNotNull(document, '.anki-card-delete-format-button'); 97 + /** @type {?import('./modal.js').Modal} */ 98 + this._cardFormatRemoveModal = null; 99 + /** @type {?import('./modal.js').Modal} */ 100 + this._cardFormatMaximumModal = null; 101 + /** @type {HTMLElement} */ 102 + this._cardFormatRemoveName = querySelectorNotNull(document, '#anki-card-format-remove-name'); 103 + /** @type {HTMLButtonElement} */ 104 + this._cardFormatRemoveConfirmButton = querySelectorNotNull(document, '#anki-card-format-remove-confirm-button'); 82 105 } 83 106 84 107 /** @type {import('./settings-controller.js').SettingsController} */ ··· 90 113 async prepare() { 91 114 /** @type {HTMLElement} */ 92 115 const ankiApiKeyInput = querySelectorNotNull(document, '#anki-api-key-input'); 93 - const ankiCardPrimaryTypeRadios = /** @type {NodeListOf<HTMLInputElement>} */ (document.querySelectorAll('input[type=radio][name=anki-card-primary-type]')); 94 116 /** @type {HTMLElement} */ 95 117 const ankiErrorLog = querySelectorNotNull(document, '#anki-error-log'); 96 - 118 + /** @type {HTMLElement} */ 119 + const newFormatButton = querySelectorNotNull(document, '#anki-cards-new-format button'); 97 120 98 121 this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false); 99 122 if (this._ankiEnableCheckbox !== null) { ··· 103 126 false, 104 127 ); 105 128 } 106 - for (const input of ankiCardPrimaryTypeRadios) { 107 - input.addEventListener('change', this._onAnkiCardPrimaryTypeRadioChange.bind(this), false); 108 - } 109 129 110 130 const testAnkiNoteViewerButtons = /** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.test-anki-note-viewer-button')); 111 131 const onTestAnkiNoteViewerButtonClick = this._onTestAnkiNoteViewerButtonClick.bind(this); ··· 130 150 } 131 151 const ankiCardFormatSettingsEntry = querySelectorNotNull(document, '[data-modal-action="show,anki-cards"]'); 132 152 ankiCardFormatSettingsEntry.addEventListener('click', onAnkiSettingChanged); 153 + 154 + /** @type {HTMLSelectElement} */ 155 + const ankiCardIconSelect = querySelectorNotNull(this._ankiCardPrimary, '.anki-card-icon'); 156 + ankiCardIconSelect.addEventListener('change', this._onIconSelectChange.bind(this), false); 157 + 158 + this._ankiCardNameInput.addEventListener('input', () => { 159 + const tabLabel = querySelectorNotNull(this._ankiCardsTabs, `.tab:nth-child(${this._cardFormatIndex + 1}) .tab-label`); 160 + tabLabel.textContent = this._ankiCardNameInput.value || `Format ${this._cardFormatIndex + 1}`; 161 + }); 162 + 163 + this._ankiCardDictionaryTypeSelect.addEventListener('change', this._onDictionaryTypeSelectChange.bind(this), false); 164 + 165 + newFormatButton.addEventListener('click', this._onNewFormatButtonClick.bind(this), false); 166 + 167 + this._cardFormatRemoveModal = this._modalController.getModal('anki-card-format-remove'); 168 + this._cardFormatMaximumModal = this._modalController.getModal('anki-add-card-format-maximum'); 169 + this._cardFormatDeleteButton.addEventListener('click', this._onCardFormatDeleteClick.bind(this), false); 170 + this._cardFormatRemoveConfirmButton.addEventListener('click', this._onCardFormatRemoveConfirm.bind(this), false); 133 171 } 134 172 135 173 /** ··· 164 202 // Private 165 203 166 204 /** */ 205 + _onIconSelectChange() { 206 + const iconSelect = /** @type {HTMLSelectElement} */ (this._ankiCardPrimary.querySelector('.anki-card-icon')); 207 + const newIcon = /** @type {import('settings').AddNoteIcon} */ (iconSelect.value); 208 + iconSelect.dataset.icon = newIcon; 209 + if (this._ankiOptions === null) { return; } 210 + this._ankiOptions.cardFormats[this._cardFormatIndex].icon = newIcon; 211 + } 212 + 213 + 214 + /** */ 167 215 async _updateOptions() { 168 216 const options = await this._settingsController.getOptions(); 169 217 const optionsContext = this._settingsController.getOptionsContext(); ··· 186 234 this._selectorObserver.disconnect(); 187 235 this._selectorObserver.observe(document.documentElement, true); 188 236 237 + this._ankiOptions = anki; 189 238 this._updateDuplicateBehavior(anki.duplicateBehavior); 239 + this._setupTabs(anki); 190 240 191 241 void this._setupFieldMenus(dictionaries); 192 242 } ··· 215 265 _onAnkiCardPrimaryTypeRadioChange(e) { 216 266 const node = /** @type {HTMLInputElement} */ (e.currentTarget); 217 267 if (!node.checked) { return; } 218 - const {value, ankiCardMenu} = node.dataset; 219 - if (typeof value !== 'string') { return; } 220 - this._setAnkiCardPrimaryType(value, ankiCardMenu); 268 + const {cardFormatIndex, ankiCardMenu} = node.dataset; 269 + if (typeof cardFormatIndex !== 'string') { return; } 270 + this._setCardFormatIndex(Number.parseInt(cardFormatIndex, 10), ankiCardMenu); 221 271 } 222 272 223 273 /** */ ··· 275 325 } 276 326 277 327 /** 278 - * @param {string} ankiCardType 328 + * @param {number} cardFormatIndex 279 329 * @param {string} [ankiCardMenu] 330 + * @throws {Error} 280 331 */ 281 - _setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) { 282 - if (this._ankiCardPrimary === null) { return; } 283 - this._ankiCardPrimary.dataset.ankiCardType = ankiCardType; 332 + _setCardFormatIndex(cardFormatIndex, ankiCardMenu) { 333 + this._cardFormatIndex = cardFormatIndex; 334 + if (this._ankiCardPrimary === null) { 335 + throw new Error('Anki card primary element not found'); 336 + } 337 + this._ankiCardPrimary.dataset.cardFormatIndex = cardFormatIndex.toString(); 284 338 if (typeof ankiCardMenu !== 'undefined') { 285 339 this._ankiCardPrimary.dataset.ankiCardMenu = ankiCardMenu; 286 340 } else { 287 341 delete this._ankiCardPrimary.dataset.ankiCardMenu; 288 342 } 343 + 344 + this._ankiCardNameInput.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', cardFormatIndex, 'name']); 345 + 346 + /** @type {HTMLSelectElement} */ 347 + const typeSelect = querySelectorNotNull(this._ankiCardPrimary, '.anki-card-type'); 348 + typeSelect.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', cardFormatIndex, 'type']); 349 + 350 + /** @type {HTMLSelectElement} */ 351 + const iconSelect = querySelectorNotNull(this._ankiCardPrimary, '.anki-card-icon'); 352 + iconSelect.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', cardFormatIndex, 'icon']); 353 + iconSelect.dataset.icon = this._ankiOptions?.cardFormats[cardFormatIndex].icon ?? 'big-circle'; 289 354 } 290 355 291 356 /** 292 - * @param {Element} node 357 + * Creates a new AnkiCardController for a node that matches the '.anki-card' selector. 358 + * This is called by the SelectorObserver when new matching nodes are added to the DOM. 359 + * @param {Element} node The DOM node that matches the '.anki-card' selector 293 360 * @returns {AnkiCardController} 294 361 */ 295 362 _createCardController(node) { ··· 321 388 async _setupFieldMenus(dictionaries) { 322 389 /** @type {[types: import('dictionary').DictionaryEntryType[], templateName: string][]} */ 323 390 const fieldMenuTargets = [ 324 - [['term'], 'anki-card-terms-field-menu'], 391 + [['term'], 'anki-card-term-field-menu'], 325 392 [['kanji'], 'anki-card-kanji-field-menu'], 326 393 [['term', 'kanji'], 'anki-card-all-field-menu'], 327 394 ]; ··· 557 624 async canAddNotes(notes) { 558 625 return await this._ankiConnect.canAddNotes(notes); 559 626 } 627 + 628 + /** 629 + * @param {import('settings').AnkiOptions} ankiOptions 630 + */ 631 + _setupTabs(ankiOptions) { 632 + const tabsContainer = this._ankiCardsTabs; 633 + 634 + while (tabsContainer.firstChild) { 635 + tabsContainer.removeChild(tabsContainer.firstChild); 636 + } 637 + 638 + if (this._cardFormatIndex > ankiOptions.cardFormats.length) { 639 + this._cardFormatIndex = ankiOptions.cardFormats.length - 1; 640 + } 641 + 642 + for (let i = 0; i < ankiOptions.cardFormats.length; ++i) { 643 + const cardFormat = ankiOptions.cardFormats[i]; 644 + const input = this._createCardFormatTab(cardFormat, i); 645 + if (i === this._cardFormatIndex) { 646 + input.checked = true; 647 + } 648 + } 649 + 650 + this._setCardFormatIndex(this._cardFormatIndex, 'anki-card-term-field-menu'); 651 + } 652 + 653 + /** 654 + * @param {import('settings').AnkiCardFormat} cardFormat 655 + * @param {number} cardFormatIndex 656 + * @returns {HTMLInputElement} 657 + */ 658 + _createCardFormatTab(cardFormat, cardFormatIndex) { 659 + const tabsContainer = this._ankiCardsTabs; 660 + const content = this._settingsController.instantiateTemplateFragment('anki-card-type-tab'); 661 + 662 + /** @type {HTMLInputElement} */ 663 + const input = querySelectorNotNull(content, 'input'); 664 + input.value = cardFormat.type; 665 + input.dataset.value = cardFormat.type; 666 + input.dataset.ankiCardMenu = `anki-card-${cardFormat.type}-field-menu`; 667 + input.dataset.cardFormatIndex = `${cardFormatIndex}`; 668 + input.addEventListener('change', this._onAnkiCardPrimaryTypeRadioChange.bind(this), false); 669 + 670 + /** @type {HTMLElement} */ 671 + const labelNode = querySelectorNotNull(content, '.tab-label'); 672 + labelNode.textContent = cardFormat.name; 673 + 674 + tabsContainer.appendChild(content); 675 + 676 + return input; 677 + } 678 + 679 + /** 680 + * @param {Event} e 681 + */ 682 + _onNewFormatButtonClick(e) { 683 + e.preventDefault(); 684 + void this._addNewFormat(); 685 + } 686 + 687 + /** */ 688 + async _addNewFormat() { 689 + const options = await this._settingsController.getOptions(); 690 + const ankiOptions = options.anki; 691 + const index = ankiOptions.cardFormats.length; 692 + 693 + if (index >= 5) { 694 + this._cardFormatMaximumModal?.setVisible(true); 695 + return; 696 + } 697 + 698 + /** @type {import('settings').AnkiCardFormat} */ 699 + const newCardFormat = { 700 + name: `Format ${index + 1}`, 701 + type: 'term', 702 + deck: '', 703 + model: '', 704 + fields: {}, 705 + icon: 'big-circle', 706 + }; 707 + 708 + await this._settingsController.modifyProfileSettings([{ 709 + action: 'splice', 710 + path: 'anki.cardFormats', 711 + start: index, 712 + deleteCount: 0, 713 + items: [newCardFormat], 714 + }]); 715 + 716 + await this._updateOptions(); 717 + } 718 + 719 + /** 720 + * @param {Event} e 721 + */ 722 + _onCardFormatDeleteClick(e) { 723 + e.preventDefault(); 724 + this.openDeleteCardFormatModal(this._cardFormatIndex); 725 + } 726 + 727 + /** 728 + * @param {number} cardFormatIndex 729 + */ 730 + openDeleteCardFormatModal(cardFormatIndex) { 731 + const cardFormat = this._getCardFormat(cardFormatIndex); 732 + if (cardFormat === null) { return; } 733 + 734 + /** @type {HTMLElement} */ (this._cardFormatRemoveName).textContent = cardFormat.name; 735 + /** @type {import('./modal.js').Modal} */ (this._cardFormatRemoveModal).node.dataset.cardFormatIndex = `${cardFormatIndex}`; 736 + /** @type {import('./modal.js').Modal} */ (this._cardFormatRemoveModal).setVisible(true); 737 + } 738 + 739 + /** */ 740 + _onCardFormatRemoveConfirm() { 741 + const modal = /** @type {import('./modal.js').Modal} */ (this._cardFormatRemoveModal); 742 + modal.setVisible(false); 743 + const {node} = modal; 744 + const cardFormatIndex = node.dataset.cardFormatIndex; 745 + delete node.dataset.cardFormatIndex; 746 + 747 + const validCardFormatIndex = this._tryGetValidCardFormatIndex(cardFormatIndex); 748 + if (validCardFormatIndex === null) { return; } 749 + 750 + void this.deleteCardFormat(validCardFormatIndex); 751 + } 752 + 753 + /** 754 + * @param {Event} e 755 + */ 756 + _onDictionaryTypeSelectChange(e) { 757 + const node = /** @type {HTMLSelectElement} */ (e.currentTarget); 758 + const value = node.value; 759 + this._ankiCardPrimary.dataset.ankiCardMenu = `anki-card-${value}-field-menu`; 760 + } 761 + 762 + /** 763 + * @param {string|undefined} stringValue 764 + * @returns {?number} 765 + */ 766 + _tryGetValidCardFormatIndex(stringValue) { 767 + if (typeof stringValue !== 'string') { return null; } 768 + const intValue = Number.parseInt(stringValue, 10); 769 + if (this._ankiOptions === null) { return null; } 770 + return ( 771 + Number.isFinite(intValue) && 772 + intValue >= 0 && 773 + intValue < this._ankiOptions.cardFormats.length ? 774 + intValue : 775 + null 776 + ); 777 + } 778 + 779 + /** 780 + * @param {number} cardFormatIndex 781 + * @returns {?import('settings').AnkiCardFormat} 782 + */ 783 + _getCardFormat(cardFormatIndex) { 784 + if (this._ankiOptions === null) { return null; } 785 + if (cardFormatIndex < 0 || cardFormatIndex >= this._ankiOptions.cardFormats.length) { return null; } 786 + return this._ankiOptions.cardFormats[cardFormatIndex]; 787 + } 788 + 789 + /** 790 + * @param {number} cardFormatIndex 791 + */ 792 + async deleteCardFormat(cardFormatIndex) { 793 + if (this._ankiOptions === null) { return; } 794 + const cardFormats = this._ankiOptions.cardFormats; 795 + if (cardFormatIndex < 0 || cardFormatIndex >= cardFormats.length) { return; } 796 + 797 + await this._settingsController.modifyProfileSettings([{ 798 + action: 'splice', 799 + path: 'anki.cardFormats', 800 + start: cardFormatIndex, 801 + deleteCount: 1, 802 + items: [], 803 + }]); 804 + 805 + this._cardFormatIndex = cardFormatIndex - 1; 806 + await this._updateOptions(); 807 + } 560 808 } 561 809 562 810 class AnkiCardController { ··· 572 820 this._ankiController = ankiController; 573 821 /** @type {HTMLElement} */ 574 822 this._node = node; 575 - const {ankiCardType} = node.dataset; 576 - /** @type {string} */ 577 - this._optionsType = typeof ankiCardType === 'string' ? ankiCardType : 'terms'; 578 - /** @type {import('dictionary').DictionaryEntryType} */ 579 - this._dictionaryEntryType = ankiCardType === 'kanji' ? 'kanji' : 'term'; 823 + const {cardFormatIndex} = node.dataset; 824 + if (typeof cardFormatIndex === 'undefined') { 825 + throw new Error('Undefined anki card type in node dataset'); 826 + } 827 + /** @type {?import('settings').AnkiCardFormat} */ 828 + this._cardFormat = null; 829 + /** @type {number} */ 830 + this._cardFormatIndex = Number.parseInt(cardFormatIndex, 10); 580 831 /** @type {string|undefined} */ 581 832 this._cardMenu = node.dataset.ankiCardMenu; 582 833 /** @type {EventListenerCollection} */ 583 834 this._eventListeners = new EventListenerCollection(); 584 835 /** @type {EventListenerCollection} */ 585 836 this._fieldEventListeners = new EventListenerCollection(); 586 - /** @type {import('settings').AnkiNoteFields} */ 837 + /** @type {import('settings').AnkiFields} */ 587 838 this._fields = {}; 588 839 /** @type {?string} */ 589 840 this._modelChangingTo = null; 590 841 /** @type {?Element} */ 591 - this._ankiCardFieldsContainer = null; 842 + this._AnkiFieldsContainer = null; 592 843 /** @type {boolean} */ 593 844 this._cleaned = false; 594 845 /** @type {import('anki-controller').FieldEntry[]} */ ··· 605 856 const ankiOptions = options.anki; 606 857 if (this._cleaned) { return; } 607 858 608 - const cardOptions = this._getCardOptions(ankiOptions, this._optionsType); 609 - if (cardOptions === null) { return; } 610 - const {deck, model, fields} = cardOptions; 859 + const cardFormat = this._getCardFormat(ankiOptions, this._cardFormatIndex); 860 + if (cardFormat === null) { return; } 861 + this._cardFormat = cardFormat; 862 + 863 + const {deck, model, fields} = this._cardFormat; 611 864 /** @type {HTMLSelectElement} */ 612 865 const deckControllerSelect = querySelectorNotNull(this._node, '.anki-card-deck'); 613 866 /** @type {HTMLSelectElement} */ ··· 616 869 this._modelController.prepare(modelControllerSelect, model); 617 870 this._fields = fields; 618 871 619 - this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields'); 620 - 872 + this._AnkiFieldsContainer = this._node.querySelector('.anki-card-fields'); 621 873 this._setupFields(); 622 874 623 875 this._eventListeners.addEventListener(this._deckController.select, 'change', this._onCardDeckChange.bind(this), false); ··· 647 899 * @returns {boolean} 648 900 */ 649 901 isStale() { 650 - return (this._optionsType !== this._node.dataset.ankiCardType); 902 + const datasetCardFormatIndex = this._node.dataset.cardFormatIndex; 903 + const datasetAnkiCardMenu = this._node.dataset.ankiCardMenu; 904 + if (typeof datasetCardFormatIndex !== 'string') { return true; } 905 + return this._cardFormatIndex !== Number.parseInt(datasetCardFormatIndex, 10) || this._cardMenu !== datasetAnkiCardMenu; 651 906 } 907 + 652 908 653 909 // Private 654 - 655 910 /** 656 911 * @param {Event} e 657 912 */ ··· 706 961 const indexNumber = typeof index === 'string' ? Number.parseInt(index, 10) : 0; 707 962 if (typeof fieldName !== 'string') { return; } 708 963 709 - const defaultValue = this._getDefaultFieldValue(fieldName, indexNumber, this._dictionaryEntryType, null); 964 + const defaultValue = this._getDefaultFieldValue(fieldName, indexNumber, this.cardFormatType, null); 710 965 if (defaultValue === '') { return; } 711 966 712 967 const match = /^\{([\w\W]+)\}$/.exec(defaultValue); ··· 764 1019 765 1020 /** 766 1021 * @param {import('settings').AnkiOptions} ankiOptions 767 - * @param {string} optionsType 768 - * @returns {?import('settings').AnkiNoteOptions} 1022 + * @param {number} cardFormatIndex 1023 + * @returns {import('settings').AnkiCardFormat} 1024 + * @throws {Error} 769 1025 */ 770 - _getCardOptions(ankiOptions, optionsType) { 771 - switch (optionsType) { 772 - case 'terms': return ankiOptions.terms; 773 - case 'kanji': return ankiOptions.kanji; 774 - default: return null; 1026 + _getCardFormat(ankiOptions, cardFormatIndex) { 1027 + const cardFormat = ankiOptions.cardFormats[cardFormatIndex]; 1028 + if (typeof cardFormat === 'undefined') { 1029 + throw new Error('Invalid card format index'); 775 1030 } 1031 + return cardFormat; 776 1032 } 777 1033 778 1034 /** */ ··· 798 1054 799 1055 /** @type {HTMLSelectElement} */ 800 1056 const overwriteSelect = querySelectorNotNull(content, '.anki-card-field-overwrite'); 801 - overwriteSelect.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._optionsType, 'fields', fieldName, 'overwriteMode']); 1057 + overwriteSelect.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', this._cardFormatIndex, 'fields', fieldName, 'overwriteMode']); 802 1058 803 1059 /** @type {HTMLInputElement} */ 804 1060 const inputField = querySelectorNotNull(content, '.anki-card-field-value'); 805 1061 inputField.value = fieldValue; 806 - inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._optionsType, 'fields', fieldName, 'value']); 1062 + inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', this._cardFormatIndex, 'fields', fieldName, 'value']); 807 1063 void this._validateFieldPermissions(inputField, index, false); 808 1064 809 1065 this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this, index), false); ··· 832 1088 } 833 1089 834 1090 const ELEMENT_NODE = Node.ELEMENT_NODE; 835 - const container = this._ankiCardFieldsContainer; 1091 + const container = this._AnkiFieldsContainer; 836 1092 if (container !== null) { 837 1093 const childNodesFrozen = [...container.childNodes]; 838 1094 for (const node of childNodesFrozen) { ··· 877 1133 878 1134 await this._settingsController.modifyProfileSettings([{ 879 1135 action: 'set', 880 - path: ObjectPropertyAccessor.getPathString(['anki', this._optionsType, 'deck']), 1136 + path: ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', this._cardFormatIndex, 'deck']), 881 1137 value, 882 1138 }]); 883 1139 } ··· 908 1164 this._modelChangingTo = null; 909 1165 } 910 1166 911 - const cardOptions = this._getCardOptions(options.anki, this._optionsType); 912 - const oldFields = cardOptions !== null ? cardOptions.fields : null; 1167 + const cardFormat = this._getCardFormat(options.anki, this._cardFormatIndex); 1168 + const oldFields = cardFormat !== null ? cardFormat.fields : null; 913 1169 914 - /** @type {import('settings').AnkiNoteFields} */ 1170 + /** @type {import('settings').AnkiFields} */ 915 1171 const fields = {}; 916 1172 for (let i = 0, ii = fieldNames.length; i < ii; ++i) { 917 1173 const fieldName = fieldNames[i]; 918 1174 fields[fieldName] = { 919 - value: this._getDefaultFieldValue(fieldName, i, this._dictionaryEntryType, oldFields), 1175 + value: this._getDefaultFieldValue(fieldName, i, cardFormat.type, oldFields), 920 1176 overwriteMode: 'coalesce', 921 1177 }; 922 1178 } ··· 925 1181 const targets = [ 926 1182 { 927 1183 action: 'set', 928 - path: ObjectPropertyAccessor.getPathString(['anki', this._optionsType, 'model']), 1184 + path: ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', this._cardFormatIndex, 'model']), 929 1185 value, 930 1186 }, 931 1187 { 932 1188 action: 'set', 933 - path: ObjectPropertyAccessor.getPathString(['anki', this._optionsType, 'fields']), 1189 + path: ObjectPropertyAccessor.getPathString(['anki', 'cardFormats', this._cardFormatIndex, 'fields']), 934 1190 value: fields, 935 1191 }, 936 1192 ]; ··· 1006 1262 * @param {string} fieldName 1007 1263 * @param {number} index 1008 1264 * @param {import('dictionary').DictionaryEntryType} dictionaryEntryType 1009 - * @param {?import('settings').AnkiNoteFields} oldFields 1265 + * @param {?import('settings').AnkiFields} oldFields 1010 1266 * @returns {string} 1011 1267 */ 1012 1268 _getDefaultFieldValue(fieldName, index, dictionaryEntryType, oldFields) { ··· 1062 1318 } 1063 1319 1064 1320 return ''; 1321 + } 1322 + 1323 + /** @type {import('dictionary').DictionaryEntryType} */ 1324 + get cardFormatType() { 1325 + if (this._cardFormat === null) { 1326 + throw new Error('Card format not initialized'); 1327 + } 1328 + return this._cardFormat.type; 1065 1329 } 1066 1330 } 1067 1331
+18 -21
ext/js/pages/settings/anki-deck-generator-controller.js
··· 129 129 const activeDeckTextConfirm = querySelectorNotNull(document, '#generate-anki-notes-active-deck-confirm'); 130 130 const options = await this._settingsController.getOptions(); 131 131 132 - this._activeNoteType = options.anki.terms.model; 133 - this._activeAnkiDeck = options.anki.terms.deck; 132 + this._activeNoteType = options.anki.cardFormats[0].model; 133 + this._activeAnkiDeck = options.anki.cardFormats[0].deck; 134 134 activeModelText.textContent = this._activeNoteType; 135 135 activeDeckText.textContent = this._activeAnkiDeck; 136 136 activeDeckTextConfirm.textContent = this._activeAnkiDeck; ··· 211 211 void this._endGenerationState(); 212 212 return; 213 213 } 214 - const noteData = await this._generateNoteData(value, 'term-kanji', false); 214 + const noteData = await this._generateNoteData(value, false); 215 215 if (noteData !== null) { 216 216 const fieldsTSV = this._fieldsToTSV(noteData.fields); 217 217 if (fieldsTSV) { ··· 268 268 void this._endGenerationState(); 269 269 return; 270 270 } 271 - const noteData = await this._generateNoteData(value, 'term-kanji', addMedia); 271 + const noteData = await this._generateNoteData(value, addMedia); 272 272 if (noteData) { 273 273 notes.push(noteData); 274 274 } ··· 368 368 369 369 /** 370 370 * @param {HTMLElement} infoNode 371 - * @param {import('anki-templates-internal').CreateModeNoTest} mode 372 371 * @param {boolean} showSuccessResult 373 372 */ 374 - async _testNoteData(infoNode, mode, showSuccessResult) { 373 + async _testNoteData(infoNode, showSuccessResult) { 375 374 /** @type {Error[]} */ 376 375 const allErrors = []; 377 376 const text = /** @type {HTMLInputElement} */ (this._renderTextInput).value; 378 377 let result; 379 378 try { 380 - const noteData = await this._generateNoteData(text, mode, false); 379 + const noteData = await this._generateNoteData(text, false); 381 380 result = noteData ? this._fieldsToTSV(noteData.fields) : `No definition found for ${text}`; 382 381 } catch (e) { 383 382 allErrors.push(toError(e)); ··· 412 411 413 412 /** 414 413 * @param {string} word 415 - * @param {import('anki-templates-internal').CreateModeNoTest} mode 416 414 * @param {boolean} addMedia 417 415 * @returns {Promise<?import('anki.js').Note>} 418 416 */ 419 - async _generateNoteData(word, mode, addMedia) { 417 + async _generateNoteData(word, addMedia) { 420 418 const optionsContext = this._settingsController.getOptionsContext(); 421 419 const data = await this._getDictionaryEntry(word, optionsContext); 422 420 if (data === null) { ··· 435 433 fullQuery: sentenceText, 436 434 }; 437 435 const template = await this._getAnkiTemplate(options); 438 - const deckOptionsFields = options.anki.terms.fields; 436 + const deckOptionsFields = options.anki.cardFormats[0].fields; 439 437 const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options; 440 - const fields = []; 441 - for (const deckField in deckOptionsFields) { 442 - if (Object.prototype.hasOwnProperty.call(deckOptionsFields, deckField)) { 443 - fields.push([deckField, deckOptionsFields[deckField]]); 444 - } 445 - } 446 438 const idleTimeout = (Number.isFinite(options.anki.downloadTimeout) && options.anki.downloadTimeout > 0 ? options.anki.downloadTimeout : null); 447 439 const languageSummary = getLanguageSummaries().find(({iso}) => iso === options.general.language); 448 440 const mediaOptions = addMedia ? {audio: {sources: options.audio.sources, preferredAudioIndex: null, idleTimeout: idleTimeout, languageSummary: languageSummary}} : null; 449 441 const requirements = addMedia ? [...this._getDictionaryEntryMedia(dictionaryEntry), {type: 'audio'}] : []; 450 442 const dictionaryStylesMap = this._ankiNoteBuilder.getDictionaryStylesMap(options.dictionaries); 443 + const cardFormat = /** @type {import('settings').AnkiCardFormat} */ ({ 444 + deck: this._activeAnkiDeck, 445 + model: this._activeNoteType, 446 + fields: deckOptionsFields, 447 + type: 'term', 448 + name: '', 449 + icon: 'big-circle', 450 + }); 451 451 const {note} = await this._ankiNoteBuilder.createNote(/** @type {import('anki-note-builder').CreateNoteDetails} */ ({ 452 452 dictionaryEntry, 453 - mode, 453 + cardFormat, 454 454 context, 455 455 template, 456 - deckName: this._activeAnkiDeck, 457 - modelName: this._activeNoteType, 458 - fields: fields, 459 456 resultOutputMode, 460 457 glossaryLayoutMode, 461 458 compactTags, ··· 533 530 534 531 const infoNode = /** @type {HTMLElement} */ (this._renderResult); 535 532 infoNode.hidden = true; 536 - void this._testNoteData(infoNode, 'term-kanji', true); 533 + void this._testNoteData(infoNode, true); 537 534 } 538 535 539 536 /** */
+18 -12
ext/js/pages/settings/anki-templates-controller.js
··· 165 165 /** */ 166 166 _onValidateCompile() { 167 167 if (this._compileResultInfo === null) { return; } 168 - void this._validate(this._compileResultInfo, '{expression}', 'term-kanji', false, true); 168 + void this._validate(this._compileResultInfo, '{expression}', false, true); 169 169 } 170 170 171 171 /** ··· 178 178 const infoNode = /** @type {HTMLElement} */ (this._renderResult); 179 179 infoNode.hidden = true; 180 180 this._cachedDictionaryEntryText = null; 181 - void this._validate(infoNode, field, 'term-kanji', true, false); 181 + void this._validate(infoNode, field, true, false); 182 182 } 183 183 184 184 /** */ ··· 238 238 /** 239 239 * @param {HTMLElement} infoNode 240 240 * @param {string} field 241 - * @param {import('anki-templates-internal').CreateModeNoTest} mode 242 241 * @param {boolean} showSuccessResult 243 242 * @param {boolean} invalidateInput 244 243 */ 245 - async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) { 244 + async _validate(infoNode, field, showSuccessResult, invalidateInput) { 246 245 /** @type {Error[]} */ 247 246 const allErrors = []; 248 247 const text = /** @type {HTMLInputElement} */ (this._renderTextInput).value; ··· 265 264 }; 266 265 const template = await this._getAnkiTemplate(options); 267 266 const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options; 267 + const fields = { 268 + field: { 269 + value: field, 270 + overwriteMode: 'skip', 271 + }, 272 + }; 273 + const cardFormat = /** @type {import('settings').AnkiCardFormat} */ ({ 274 + type: 'term', 275 + name: '', 276 + deck: '', 277 + model: '', 278 + fields, 279 + icon: 'big-circle', 280 + }); 268 281 const {note, errors} = await this._ankiNoteBuilder.createNote(/** @type {import('anki-note-builder').CreateNoteDetails} */ ({ 269 282 dictionaryEntry, 270 - mode, 271 283 context, 272 284 template, 273 - deckName: '', 274 - modelName: '', 275 - fields: [ 276 - [ 277 - 'field', {value: field}, 278 - ], 279 - ], 285 + cardFormat, 280 286 resultOutputMode, 281 287 glossaryLayoutMode, 282 288 compactTags,
+7 -5
ext/js/pages/settings/dictionary-import-controller.js
··· 651 651 const {profiles} = options; 652 652 653 653 for (const profile of profiles) { 654 - const ankiTermFields = profile.options.anki.terms.fields; 655 - const oldFieldSegmentRegex = new RegExp(getKebabCase(profilesDictionarySettings[profile.id].name), 'g'); 656 - const newFieldSegment = getKebabCase(result.title); 657 - for (const key of Object.keys(ankiTermFields)) { 658 - ankiTermFields[key].value = ankiTermFields[key].value.replace(oldFieldSegmentRegex, newFieldSegment); 654 + for (const cardFormat of profile.options.anki.cardFormats) { 655 + const ankiTermFields = cardFormat.fields; 656 + const oldFieldSegmentRegex = new RegExp(getKebabCase(profilesDictionarySettings[profile.id].name), 'g'); 657 + const newFieldSegment = getKebabCase(result.title); 658 + for (const key of Object.keys(ankiTermFields)) { 659 + ankiTermFields[key].value = ankiTermFields[key].value.replace(oldFieldSegmentRegex, newFieldSegment); 660 + } 659 661 } 660 662 } 661 663 await this._settingsController.setAllSettings(options);
+43 -24
ext/js/pages/settings/keyboard-shortcuts-controller.js
··· 61 61 ['historyForward', {scopes: new Set(['popup', 'search'])}], 62 62 ['profilePrevious', {scopes: new Set(['popup', 'search', 'web'])}], 63 63 ['profileNext', {scopes: new Set(['popup', 'search', 'web'])}], 64 - ['addNoteKanji', {scopes: new Set(['popup', 'search'])}], 65 - ['addNoteTermKanji', {scopes: new Set(['popup', 'search'])}], 66 - ['addNoteTermKana', {scopes: new Set(['popup', 'search'])}], 67 - ['viewNotes', {scopes: new Set(['popup', 'search'])}], 64 + ['addNote', {scopes: new Set(['popup', 'search']), argument: {template: 'hotkey-argument-anki-card-format', default: '0'}}], 65 + ['viewNotes', {scopes: new Set(['popup', 'search']), argument: {template: 'hotkey-argument-anki-card-format', default: '0'}}], 68 66 ['playAudio', {scopes: new Set(['popup', 'search'])}], 69 67 ['playAudioFromSource', {scopes: new Set(['popup', 'search']), argument: {template: 'hotkey-argument-audio-source', default: 'jpod101'}}], 70 68 ['copyHostSelection', {scopes: new Set(['popup'])}], ··· 159 157 return this._actionDetails.get(action); 160 158 } 161 159 160 + /** 161 + * @returns {Promise<string[]>} 162 + */ 163 + async getAnkiCardFormats() { 164 + const options = await this._settingsController.getOptions(); 165 + const {anki} = options; 166 + return anki.cardFormats.map((cardFormat) => cardFormat.name); 167 + } 168 + 162 169 // Private 163 170 164 171 /** 165 172 * @param {import('settings-controller').EventArgument<'optionsChanged'>} details 166 173 */ 167 - _onOptionsChanged({options}) { 174 + async _onOptionsChanged({options}) { 168 175 for (const entry of this._entries) { 169 176 entry.cleanup(); 170 177 } ··· 180 187 fragment.appendChild(node); 181 188 const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, os, this._stringComparer); 182 189 this._entries.push(entry); 183 - entry.prepare(); 190 + await entry.prepare(); 184 191 } 185 192 186 193 const listContainer = /** @type {HTMLElement} */ (this._listContainer); ··· 223 230 async _updateOptions() { 224 231 const options = await this._settingsController.getOptions(); 225 232 const optionsContext = this._settingsController.getOptionsContext(); 226 - this._onOptionsChanged({options, optionsContext}); 233 + await this._onOptionsChanged({options, optionsContext}); 227 234 } 228 235 229 236 /** */ ··· 279 286 } 280 287 281 288 /** */ 282 - prepare() { 289 + async prepare() { 283 290 const node = this._node; 284 291 285 292 /** @type {HTMLButtonElement} */ ··· 308 315 enabledToggle.dataset.setting = `${this._basePath}.enabled`; 309 316 310 317 this._updateScopesButton(); 311 - this._updateActionArgument(); 318 + await this._updateActionArgument(); 312 319 313 320 this._eventListeners.addEventListener(scopesButton, 'menuOpen', this._onScopesMenuOpen.bind(this)); 314 321 this._eventListeners.addEventListener(scopesButton, 'menuClose', this._onScopesMenuClose.bind(this)); ··· 596 603 597 604 this._updateScopesButton(); 598 605 this._updateScopesMenu(); 599 - this._updateActionArgument(); 606 + await this._updateActionArgument(); 600 607 } 601 608 602 609 /** ··· 687 694 } 688 695 689 696 /** */ 690 - _updateActionArgument() { 697 + async _updateActionArgument() { 691 698 this._clearArgumentEventListeners(); 692 699 693 700 const {action, argument} = this._data; ··· 697 704 if (this._argumentContainer !== null) { 698 705 this._argumentContainer.textContent = ''; 699 706 } 700 - if (typeof argumentDetails !== 'undefined') { 701 - const {template} = argumentDetails; 702 - const node = this._parent.settingsController.instantiateTemplate(template); 703 - const inputSelector = '.hotkey-argument-input'; 704 - const inputNode = /** @type {HTMLInputElement} */ (node.matches(inputSelector) ? node : node.querySelector(inputSelector)); 705 - if (inputNode !== null) { 706 - this._setArgumentInputValue(inputNode, argument); 707 - this._argumentInput = inputNode; 708 - void this._updateArgumentInputValidity(); 709 - this._argumentEventListeners.addEventListener(inputNode, 'change', this._onArgumentValueChange.bind(this, template), false); 710 - } 711 - if (this._argumentContainer !== null) { 712 - this._argumentContainer.appendChild(node); 707 + if (typeof argumentDetails === 'undefined') { 708 + return; 709 + } 710 + const {template} = argumentDetails; 711 + const node = this._parent.settingsController.instantiateTemplate(template); 712 + const inputSelector = '.hotkey-argument-input'; 713 + const inputNode = /** @type {HTMLInputElement} */ (node.matches(inputSelector) ? node : node.querySelector(inputSelector)); 714 + if (inputNode !== null) { 715 + this._setArgumentInputValue(inputNode, argument); 716 + this._argumentInput = inputNode; 717 + void this._updateArgumentInputValidity(); 718 + this._argumentEventListeners.addEventListener(inputNode, 'change', this._onArgumentValueChange.bind(this, template), false); 719 + } 720 + if (template === 'hotkey-argument-anki-card-format') { 721 + const ankiCardFormats = await this._parent.getAnkiCardFormats(); 722 + const selectNode = /** @type {HTMLSelectElement} */ (node.querySelector('.anki-card-format-select')); 723 + for (const [index, format] of ankiCardFormats.entries()) { 724 + const option = document.createElement('option'); 725 + option.value = `${index}`; 726 + option.textContent = format; 727 + selectNode.appendChild(option); 713 728 } 729 + selectNode.value = argument; 730 + } 731 + if (this._argumentContainer !== null) { 732 + this._argumentContainer.appendChild(node); 714 733 } 715 734 } 716 735
+1 -1
ext/js/pages/settings/settings-main.js
··· 124 124 const settingsBackup = new BackupController(settingsController, modalController); 125 125 preparePromises.push(settingsBackup.prepare()); 126 126 127 - const ankiController = new AnkiController(settingsController, application); 127 + const ankiController = new AnkiController(settingsController, application, modalController); 128 128 preparePromises.push(ankiController.prepare()); 129 129 130 130 const ankiDeckGeneratorController = new AnkiDeckGeneratorController(application, settingsController, modalController, ankiController);
+3 -3
ext/settings.html
··· 1985 1985 </div> 1986 1986 <div class="settings-item settings-item-button" data-modal-action="show,anki-cards"><div class="settings-item-inner"> 1987 1987 <div class="settings-item-left"> 1988 - <div class="settings-item-label">Configure Anki card format&hellip;</div> 1988 + <div class="settings-item-label">Configure Anki flashcards&hellip;</div> 1989 1989 </div> 1990 1990 <div class="settings-item-right open-panel-button-container"> 1991 1991 <button type="button" class="icon-button"><span class="icon-button-inner"><span class="icon" data-icon="material-right-arrow"></span></span></button> ··· 1993 1993 </div></div> 1994 1994 <div class="settings-item settings-item-button advanced-only" data-modal-action="show,anki-card-templates"><div class="settings-item-inner"> 1995 1995 <div class="settings-item-left"> 1996 - <div class="settings-item-label">Configure Anki card templates&hellip;</div> 1996 + <div class="settings-item-label">Customize handlebars templates&hellip;</div> 1997 1997 </div> 1998 1998 <div class="settings-item-right open-panel-button-container"> 1999 1999 <button type="button" class="icon-button"><span class="icon-button-inner"><span class="icon" data-icon="material-right-arrow"></span></span></button> ··· 2001 2001 </div></div> 2002 2002 <div class="settings-item settings-item-button advanced-only" data-modal-action="show,generate-anki-notes" id="generate-anki-notes-main-settings-entry"><div class="settings-item-inner"> 2003 2003 <div class="settings-item-left"> 2004 - <div class="settings-item-label">Generate Anki Notes (Experimental)&hellip;</div> 2004 + <div class="settings-item-label">Generate notes (experimental)&hellip;</div> 2005 2005 </div> 2006 2006 <div class="settings-item-right open-panel-button-container"> 2007 2007 <button type="button" class="icon-button"><span class="icon-button-inner"><span class="icon" data-icon="material-right-arrow"></span></span></button>
+34 -29
ext/templates-display.html
··· 5 5 <div class="entry-current-indicator" title="Current entry"><span class="entry-current-indicator-inner"></span></div> 6 6 <div class="entry-header"> 7 7 <div class="actions"> 8 - <button type="button" class="action-button action-button-collapsible" data-action="view-flags" hidden disabled> 9 - <span class="action-icon icon" data-icon="flag"></span> 10 - </button> 11 - <button type="button" class="action-button action-button-collapsible" data-action="view-tags" hidden disabled> 12 - <span class="action-icon icon" data-icon="tag"></span> 13 - </button> 14 - <button type="button" class="action-button" data-action="view-note" hidden disabled title="View added note" data-hotkey='["viewNotes","title","View added note ({0})"]' data-menu-position="left below h-cover v-cover"> 15 - <span class="action-icon icon color-icon" data-icon="view-note"></span> 16 - <span class="action-button-badge icon" hidden></span> 17 - </button> 18 - <button type="button" class="action-button" data-action="save-note" hidden disabled data-mode="term-kanji" title="Add expression" data-hotkey='["addNoteTermKanji","title","Add expression ({0})"]'> 19 - <span class="action-icon icon color-icon" data-icon="add-term-kanji"></span> 20 - </button> 21 - <button type="button" class="action-button" data-action="save-note" hidden disabled data-mode="term-kana" title="Add reading" data-hotkey='["addNoteTermKana","title","Add reading ({0})"]'> 22 - <span class="action-icon icon color-icon" data-icon="add-term-kana"></span> 23 - </button> 24 - <button type="button" class="action-button" data-action="play-audio" title="Play audio" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]' data-menu-position="left below h-cover v-cover"> 25 - <span class="action-icon icon color-icon" data-icon="play-audio"></span> 26 - <span class="action-button-badge icon" hidden></span> 27 - </button> 8 + <div class="note-actions-container"></div> 9 + <div class="action-button-container"> 10 + <button type="button" class="action-button" data-action="play-audio" title="Play audio" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]' data-menu-position="left below h-cover v-cover"> 11 + <span class="action-icon icon color-icon" data-icon="play-audio"></span> 12 + <span class="action-button-badge icon" hidden></span> 13 + </button> 14 + <button type="button" class="action-button action-button-collapsible" data-action="menu" data-menu-position="left below h-cover v-cover"> 15 + <span class="action-icon icon" data-icon="kebab-menu"></span> 16 + </button> 17 + </div> 28 18 <span class="entry-current-indicator-icon" title="Current entry"> 29 19 <span class="icon color-icon" data-icon="entry-current"></span> 30 20 </span> 31 - <button type="button" class="action-button action-button-collapsible" data-action="menu" data-menu-position="left below h-cover v-cover"> 32 - <span class="action-icon icon" data-icon="kebab-menu"></span> 33 - </button> 34 21 </div> 35 22 <div class="headword-list"></div> 36 23 <div class="headword-list-details"> ··· 114 101 <div class="entry-current-indicator" title="Current entry"><span class="entry-current-indicator-inner"></span></div> 115 102 <div class="entry-header"> 116 103 <div class="actions"> 117 - <button type="button" class="action-button" data-action="view-note" hidden disabled title="View added note" data-hotkey='["viewNotes","title","View added note ({0})"]'> 118 - <span class="action-icon icon color-icon" data-icon="view-note"></span> 119 - </button> 120 - <button type="button" class="action-button" data-action="save-note" hidden disabled data-mode="kanji" title="Add kanji" data-hotkey='["addNoteKanji","title","Add kanji ({0})"]'> 121 - <span class="action-icon icon color-icon" data-icon="add-kanji"></span> 122 - </button> 104 + <div class="note-actions-container"></div> 123 105 <span class="entry-current-indicator-icon" title="Current entry"> 124 106 <span class="icon color-icon" data-icon="entry-current"></span> 125 107 </span> ··· 209 191 <template id="view-note-button-popup-menu-item-template"><button type="button" class="popup-menu-item"><span class="popup-menu-item-label"></span></button></template> 210 192 <template id="dictionary-entry-popup-menu-template"><div class="popup-menu-container scan-disable dictionary-entry-popup-menu" tabindex="-1" role="dialog"><div class="popup-menu popup-menu-auto-size"><div class="popup-menu-body"></div></div></div></template> 211 193 <template id="dictionary-entry-popup-menu-item-template"><button type="button" class="popup-menu-item"><span class="popup-menu-item-label"></span></button></template> 194 + 195 + <!-- Note action buttons --> 196 + <template id="note-action-button-view-flags-template"><button type="button" class="action-button action-button-collapsible" data-action="view-flags" hidden disabled> 197 + <span class="action-icon icon" data-icon="flag"></span> 198 + </button></template> 199 + 200 + <template id="note-action-button-view-tags-template"><button type="button" class="action-button action-button-collapsible" data-action="view-tags" hidden disabled> 201 + <span class="action-icon icon" data-icon="tag"></span> 202 + </button></template> 203 + 204 + <template id="note-action-button-view-note-template"><button type="button" class="action-button" data-action="view-note" hidden disabled title="View added note" data-hotkey='["viewNotes","title","View added note ({0})"]' data-menu-position="left below h-cover v-cover"> 205 + <span class="action-icon icon color-icon" data-icon="view-note"></span> 206 + <span class="action-button-badge icon"></span> 207 + </button></template> 208 + 209 + <template id="action-button-container-template"> 210 + <div class="action-button-container"> 211 + <button type="button" class="action-button" data-action="save-note" title="Add {0}" data-hotkey='["saveNote","title","Add {0} ({0})"]'> 212 + <span class="action-icon icon color-icon" data-icon=""></span> 213 + </button> 214 + </div> 215 + </template> 216 + 212 217 213 218 </body></html>
+63 -21
ext/templates-modals.html
··· 822 822 823 823 824 824 <!-- Anki cards modal --> 825 - <div id="anki-cards-modal" class="modal" tabindex="-1" role="dialog" hidden><div class="modal-content"> 825 + <div id="anki-cards-modal" class="modal" tabindex="-1" role="dialog" hidden><div class="modal-content modal-content-full"> 826 826 <div class="modal-header"> 827 827 <div class="modal-title">Anki Cards</div> 828 - <div class="modal-header-button-container"> 829 - <div class="modal-header-button-group"> 830 - <button type="button" class="icon-button modal-header-button" data-modal-action="expand"><span class="icon-button-inner"><span class="icon" data-icon="expand"></span></span></button> 831 - <button type="button" class="icon-button modal-header-button" data-modal-action="collapse"><span class="icon-button-inner"><span class="icon" data-icon="collapse"></span></span></button> 832 - </div> 833 - </div> 834 828 </div> 835 829 <div> 836 - <div class="tabs-container"> 837 - <div class="tabs"> 838 - <label class="tab"> 839 - <input type="radio" name="anki-card-primary-type" data-value="terms" data-anki-card-menu="anki-card-terms-field-menu" checked> 840 - <div class="tab-inner"><span class="tab-label">Terms</span></div> 841 - </label> 842 - <label class="tab"> 843 - <input type="radio" name="anki-card-primary-type" data-value="kanji" data-anki-card-menu="anki-card-kanji-field-menu"> 844 - <div class="tab-inner"><span class="tab-label">Kanji</span></div> 845 - </label> 846 - </div> 847 - <div class="tabs-right" hidden> 848 - <button type="button" class="icon-button" data-menu-position="below left" id="anki-card-primary-type-menu-button"><span class="icon-button-inner"><span class="icon" data-icon="kebab-menu"></span></span></button> 830 + <div class="anki-card-tabs-container"> 831 + <div class="tabs" id="anki-cards-tabs"></div> 832 + <div id="anki-cards-new-format" class="advanced-only"> 833 + <button type="button" class="icon-button"><span class="icon-button-inner"><span class="icon" data-icon="plus-thick"></span></span></button> 849 834 </div> 850 835 </div> 851 836 <div class="modal-separator-line"></div> 852 837 </div> 853 - <div class="modal-body anki-card" id="anki-card-primary" data-anki-card-type="terms" data-anki-card-menu="anki-card-terms-field-menu"> 838 + <div class="modal-body anki-card" id="anki-card-primary" data-card-format-index="0" data-anki-card-menu="anki-card-term-field-menu"> 839 + <div class="settings-item advanced-only"><div class="settings-item-inner"> 840 + <div class="settings-item-left"> 841 + <div class="settings-item-label">Name</div> 842 + </div> 843 + <div class="settings-item-right"> 844 + <input type="text" class="anki-card-name" data-setting="anki.cardFormats.[0].name"> 845 + </div> 846 + </div></div> 847 + <div class="settings-item advanced-only"><div class="settings-item-inner"> 848 + <div class="settings-item-left"> 849 + <div class="settings-item-label">Dictionary Type</div> 850 + </div> 851 + <div class="settings-item-right"> 852 + <select class="anki-card-type"> 853 + <option value="term">Term</option> 854 + <option value="kanji">Kanji</option> 855 + </select> 856 + </div> 857 + </div></div> 858 + <div class="settings-item advanced-only"><div class="settings-item-inner"> 859 + <div class="settings-item-left"> 860 + <div class="settings-item-label">Button</div> 861 + </div> 862 + <div class="settings-item-right"> 863 + <select class="anki-card-icon select-icon icon color-icon" data-icon="big-circle"> 864 + <option value="big-circle">Big Circle</option> 865 + <option value="small-circle">Small Circle</option> 866 + <option value="big-square">Square</option> 867 + <option value="big-diamond">Diamond</option> 868 + </select> 869 + </div> 870 + </div></div> 854 871 <div class="settings-item"><div class="settings-item-inner"> 855 872 <div class="settings-item-left"> 856 873 <div class="settings-item-label">Deck</div> ··· 867 884 <select class="anki-card-model"></select> 868 885 </div> 869 886 </div></div> 887 + <hr> 870 888 <div class="anki-card-fields"> 871 889 <div class="anki-card-field-name-header" data-persistent="true">Field</div> 872 890 <div class="anki-card-field-input-header" data-persistent="true">Value</div> ··· 874 892 </div> 875 893 </div> 876 894 <div class="modal-footer"> 895 + <button type="button" class="danger anki-card-delete-format-button advanced-only">Delete Format</button> 877 896 <button type="button" class="low-emphasis" data-modal-action="show,anki-cards-info">Help</button> 878 897 <button type="button" data-modal-action="hide">Close</button> 879 898 </div> ··· 1154 1173 <button type="button" data-modal-action="hide">Close</button> 1155 1174 </div> 1156 1175 </div></div> 1176 + 1177 + <div id="anki-card-format-remove-modal" class="modal" tabindex="-1" role="dialog" hidden><div class="modal-content modal-content-small"> 1178 + <div class="modal-header"><div class="modal-title">Confirm Card Format Deletion</div></div> 1179 + <div class="modal-body"> 1180 + <p>Are you sure you want to delete the <em id="anki-card-format-remove-name"></em> card format?</p> 1181 + </div> 1182 + <div class="modal-footer"> 1183 + <button type="button" class="low-emphasis" data-modal-action="hide">Cancel</button> 1184 + <button type="button" class="danger" id="anki-card-format-remove-confirm-button">Delete Format</button> 1185 + </div> 1186 + </div></div> 1187 + 1188 + <div id="anki-add-card-format-maximum-modal" class="modal" tabindex="-1" role="dialog" hidden><div class="modal-content modal-content-small"> 1189 + <div class="modal-header"><div class="modal-title">Maximum Card Formats Reached</div></div> 1190 + <div class="modal-body"> 1191 + <p>The number of card formats is limited to 5.</p> 1192 + </div> 1193 + <div class="modal-footer"> 1194 + <button type="button" class="low-emphasis" data-modal-action="hide">Close</button> 1195 + </div> 1196 + </div></div> 1197 + 1198 + 1157 1199 1158 1200 1159 1201 <!-- Anki field template modals -->
+14 -4
ext/templates-settings.html
··· 287 287 </div></div></div></template> 288 288 289 289 <!-- Anki card --> 290 + <template id="anki-card-type-tab-template"> 291 + <label class="tab"> 292 + <input type="radio" name="anki-card-primary-type" data-value="terms" data-anki-card-menu="anki-card-term-field-menu"> 293 + <div class="tab-inner"><span class="tab-label"></span></div> 294 + </label> 295 + </template> 296 + 290 297 <template id="anki-card-field-template"> 291 298 <div class="anki-card-field-name-container"> 292 299 <span class="anki-card-field-name"></span> ··· 312 319 </select> 313 320 </div> 314 321 </template> 315 - <template id="anki-card-terms-field-menu-template"><div class="popup-menu-container" tabindex="-1" role="dialog"><div class="popup-menu"><div class="popup-menu-body"></div></div></div></template> 322 + <template id="anki-card-term-field-menu-template"><div class="popup-menu-container" tabindex="-1" role="dialog"><div class="popup-menu"><div class="popup-menu-body"></div></div></div></template> 316 323 <template id="anki-card-kanji-field-menu-template"><div class="popup-menu-container" tabindex="-1" role="dialog"><div class="popup-menu"><div class="popup-menu-body"></div></div></div></template> 317 324 <template id="anki-card-all-field-menu-template"><div class="popup-menu-container" tabindex="-1" role="dialog"><div class="popup-menu"><div class="popup-menu-body"></div></div></div></template> 318 325 ··· 424 431 <option value="historyForward">Navigate forward in history</option> 425 432 <option value="profilePrevious">Switch to previous profile</option> 426 433 <option value="profileNext">Switch to next profile</option> 427 - <option value="addNoteKanji">Add kanji note</option> 428 - <option value="addNoteTermKanji">Add term note</option> 429 - <option value="addNoteTermKana">Add term note (reading)</option> 434 + <option value="addNote">Add note</option> 430 435 <option value="viewNotes">View notes</option> 431 436 <option value="playAudio">Play audio</option> 432 437 <option value="playAudioFromSource">Play audio from source</option> ··· 493 498 <option value="text-to-speech" title="Not supported in Anki">Text-to-speech ⚠️</option> 494 499 <option value="text-to-speech-reading" title="Not supported in Anki">Text-to-speech (Kana reading) ⚠️</option> 495 500 <option value="custom">Custom</option> 501 + </select> 502 + </div></template> 503 + <template id="hotkey-argument-anki-card-format-template"><div class="flex-row-nowrap"> 504 + <span class="hotkey-argument-label">Format:</span> 505 + <select class="anki-card-format-select hotkey-argument-input horizontal-flex-fill"> 496 506 </select> 497 507 </div></template> 498 508
+8 -1
test/anki-template-renderer.test.js
··· 39 39 frequencies: [], 40 40 }, 41 41 resultOutputMode: 'split', 42 - mode: 'test', 42 + cardFormat: { 43 + type: 'term', 44 + name: 'test', 45 + deck: 'deck', 46 + model: 'model', 47 + fields: {}, 48 + icon: 'big-circle', 49 + }, 43 50 glossaryLayoutMode: 'default', 44 51 compactTags: false, 45 52 context: {
-327
test/data/translator-test-results-note-data1.json
··· 147 147 "compactTags": false, 148 148 "group": false, 149 149 "merge": false, 150 - "modeTermKanji": false, 151 - "modeTermKana": false, 152 - "modeKanji": false, 153 150 "compactGlossaries": false, 154 151 "uniqueExpressions": [], 155 152 "uniqueReadings": [], ··· 314 311 "compactTags": false, 315 312 "group": false, 316 313 "merge": false, 317 - "modeTermKanji": false, 318 - "modeTermKana": false, 319 - "modeKanji": false, 320 314 "compactGlossaries": false, 321 315 "uniqueExpressions": [], 322 316 "uniqueReadings": [], ··· 635 629 "compactTags": false, 636 630 "group": false, 637 631 "merge": false, 638 - "modeTermKanji": false, 639 - "modeTermKana": false, 640 - "modeKanji": false, 641 632 "compactGlossaries": false, 642 633 "uniqueExpressions": [ 643 634 "打" ··· 960 951 "compactTags": false, 961 952 "group": false, 962 953 "merge": false, 963 - "modeTermKanji": false, 964 - "modeTermKana": false, 965 - "modeKanji": false, 966 954 "compactGlossaries": false, 967 955 "uniqueExpressions": [ 968 956 "打" ··· 1281 1269 "compactTags": false, 1282 1270 "group": false, 1283 1271 "merge": false, 1284 - "modeTermKanji": false, 1285 - "modeTermKana": false, 1286 - "modeKanji": false, 1287 1272 "compactGlossaries": false, 1288 1273 "uniqueExpressions": [ 1289 1274 "打つ" ··· 1597 1582 "compactTags": false, 1598 1583 "group": false, 1599 1584 "merge": false, 1600 - "modeTermKanji": false, 1601 - "modeTermKana": false, 1602 - "modeKanji": false, 1603 1585 "compactGlossaries": false, 1604 1586 "uniqueExpressions": [ 1605 1587 "打つ" ··· 1913 1895 "compactTags": false, 1914 1896 "group": false, 1915 1897 "merge": false, 1916 - "modeTermKanji": false, 1917 - "modeTermKana": false, 1918 - "modeKanji": false, 1919 1898 "compactGlossaries": false, 1920 1899 "uniqueExpressions": [ 1921 1900 "打つ" ··· 2229 2208 "compactTags": false, 2230 2209 "group": false, 2231 2210 "merge": false, 2232 - "modeTermKanji": false, 2233 - "modeTermKana": false, 2234 - "modeKanji": false, 2235 2211 "compactGlossaries": false, 2236 2212 "uniqueExpressions": [ 2237 2213 "打つ" ··· 2545 2521 "compactTags": false, 2546 2522 "group": false, 2547 2523 "merge": false, 2548 - "modeTermKanji": false, 2549 - "modeTermKana": false, 2550 - "modeKanji": false, 2551 2524 "compactGlossaries": false, 2552 2525 "uniqueExpressions": [ 2553 2526 "打" ··· 2870 2843 "compactTags": false, 2871 2844 "group": false, 2872 2845 "merge": false, 2873 - "modeTermKanji": false, 2874 - "modeTermKana": false, 2875 - "modeKanji": false, 2876 2846 "compactGlossaries": false, 2877 2847 "uniqueExpressions": [ 2878 2848 "打" ··· 3264 3234 "compactTags": false, 3265 3235 "group": false, 3266 3236 "merge": false, 3267 - "modeTermKanji": false, 3268 - "modeTermKana": false, 3269 - "modeKanji": false, 3270 3237 "compactGlossaries": false, 3271 3238 "uniqueExpressions": [ 3272 3239 "打ち込む" ··· 3688 3655 "compactTags": false, 3689 3656 "group": false, 3690 3657 "merge": false, 3691 - "modeTermKanji": false, 3692 - "modeTermKana": false, 3693 - "modeKanji": false, 3694 3658 "compactGlossaries": false, 3695 3659 "uniqueExpressions": [ 3696 3660 "打ち込む" ··· 4112 4076 "compactTags": false, 4113 4077 "group": false, 4114 4078 "merge": false, 4115 - "modeTermKanji": false, 4116 - "modeTermKana": false, 4117 - "modeKanji": false, 4118 4079 "compactGlossaries": false, 4119 4080 "uniqueExpressions": [ 4120 4081 "打ち込む" ··· 4536 4497 "compactTags": false, 4537 4498 "group": false, 4538 4499 "merge": false, 4539 - "modeTermKanji": false, 4540 - "modeTermKana": false, 4541 - "modeKanji": false, 4542 4500 "compactGlossaries": false, 4543 4501 "uniqueExpressions": [ 4544 4502 "打ち込む" ··· 4892 4850 "compactTags": false, 4893 4851 "group": false, 4894 4852 "merge": false, 4895 - "modeTermKanji": false, 4896 - "modeTermKana": false, 4897 - "modeKanji": false, 4898 4853 "compactGlossaries": false, 4899 4854 "uniqueExpressions": [ 4900 4855 "打つ" ··· 5213 5168 "compactTags": false, 5214 5169 "group": false, 5215 5170 "merge": false, 5216 - "modeTermKanji": false, 5217 - "modeTermKana": false, 5218 - "modeKanji": false, 5219 5171 "compactGlossaries": false, 5220 5172 "uniqueExpressions": [ 5221 5173 "打つ" ··· 5534 5486 "compactTags": false, 5535 5487 "group": false, 5536 5488 "merge": false, 5537 - "modeTermKanji": false, 5538 - "modeTermKana": false, 5539 - "modeKanji": false, 5540 5489 "compactGlossaries": false, 5541 5490 "uniqueExpressions": [ 5542 5491 "打つ" ··· 5855 5804 "compactTags": false, 5856 5805 "group": false, 5857 5806 "merge": false, 5858 - "modeTermKanji": false, 5859 - "modeTermKana": false, 5860 - "modeKanji": false, 5861 5807 "compactGlossaries": false, 5862 5808 "uniqueExpressions": [ 5863 5809 "打つ" ··· 6171 6117 "compactTags": false, 6172 6118 "group": false, 6173 6119 "merge": false, 6174 - "modeTermKanji": false, 6175 - "modeTermKana": false, 6176 - "modeKanji": false, 6177 6120 "compactGlossaries": false, 6178 6121 "uniqueExpressions": [ 6179 6122 "打" ··· 6496 6439 "compactTags": false, 6497 6440 "group": false, 6498 6441 "merge": false, 6499 - "modeTermKanji": false, 6500 - "modeTermKana": false, 6501 - "modeKanji": false, 6502 6442 "compactGlossaries": false, 6503 6443 "uniqueExpressions": [ 6504 6444 "打" ··· 6660 6600 "compactTags": false, 6661 6601 "group": false, 6662 6602 "merge": false, 6663 - "modeTermKanji": false, 6664 - "modeTermKana": false, 6665 - "modeKanji": false, 6666 6603 "compactGlossaries": false, 6667 6604 "uniqueExpressions": [ 6668 6605 "画像" ··· 6981 6918 "compactTags": false, 6982 6919 "group": false, 6983 6920 "merge": false, 6984 - "modeTermKanji": false, 6985 - "modeTermKana": false, 6986 - "modeKanji": false, 6987 6921 "compactGlossaries": false, 6988 6922 "uniqueExpressions": [ 6989 6923 "打" ··· 7311 7245 "compactTags": false, 7312 7246 "group": false, 7313 7247 "merge": false, 7314 - "modeTermKanji": false, 7315 - "modeTermKana": false, 7316 - "modeKanji": false, 7317 7248 "compactGlossaries": false, 7318 7249 "uniqueExpressions": [ 7319 7250 "打" ··· 7627 7558 "compactTags": false, 7628 7559 "group": false, 7629 7560 "merge": false, 7630 - "modeTermKanji": false, 7631 - "modeTermKana": false, 7632 - "modeKanji": false, 7633 7561 "compactGlossaries": false, 7634 7562 "uniqueExpressions": [ 7635 7563 "打" ··· 7948 7876 "compactTags": false, 7949 7877 "group": false, 7950 7878 "merge": false, 7951 - "modeTermKanji": false, 7952 - "modeTermKana": false, 7953 - "modeKanji": false, 7954 7879 "compactGlossaries": false, 7955 7880 "uniqueExpressions": [ 7956 7881 "打つ" ··· 8264 8189 "compactTags": false, 8265 8190 "group": false, 8266 8191 "merge": false, 8267 - "modeTermKanji": false, 8268 - "modeTermKana": false, 8269 - "modeKanji": false, 8270 8192 "compactGlossaries": false, 8271 8193 "uniqueExpressions": [ 8272 8194 "打つ" ··· 8585 8507 "compactTags": false, 8586 8508 "group": false, 8587 8509 "merge": false, 8588 - "modeTermKanji": false, 8589 - "modeTermKana": false, 8590 - "modeKanji": false, 8591 8510 "compactGlossaries": false, 8592 8511 "uniqueExpressions": [ 8593 8512 "打つ" ··· 8901 8820 "compactTags": false, 8902 8821 "group": false, 8903 8822 "merge": false, 8904 - "modeTermKanji": false, 8905 - "modeTermKana": false, 8906 - "modeKanji": false, 8907 8823 "compactGlossaries": false, 8908 8824 "uniqueExpressions": [ 8909 8825 "打つ" ··· 9295 9211 "compactTags": false, 9296 9212 "group": false, 9297 9213 "merge": false, 9298 - "modeTermKanji": false, 9299 - "modeTermKana": false, 9300 - "modeKanji": false, 9301 9214 "compactGlossaries": false, 9302 9215 "uniqueExpressions": [ 9303 9216 "打ち込む" ··· 9719 9632 "compactTags": false, 9720 9633 "group": false, 9721 9634 "merge": false, 9722 - "modeTermKanji": false, 9723 - "modeTermKana": false, 9724 - "modeKanji": false, 9725 9635 "compactGlossaries": false, 9726 9636 "uniqueExpressions": [ 9727 9637 "打ち込む" ··· 10075 9985 "compactTags": false, 10076 9986 "group": false, 10077 9987 "merge": false, 10078 - "modeTermKanji": false, 10079 - "modeTermKana": false, 10080 - "modeKanji": false, 10081 9988 "compactGlossaries": false, 10082 9989 "uniqueExpressions": [ 10083 9990 "打つ" ··· 10396 10303 "compactTags": false, 10397 10304 "group": false, 10398 10305 "merge": false, 10399 - "modeTermKanji": false, 10400 - "modeTermKana": false, 10401 - "modeKanji": false, 10402 10306 "compactGlossaries": false, 10403 10307 "uniqueExpressions": [ 10404 10308 "打つ" ··· 10790 10694 "compactTags": false, 10791 10695 "group": false, 10792 10696 "merge": false, 10793 - "modeTermKanji": false, 10794 - "modeTermKana": false, 10795 - "modeKanji": false, 10796 10697 "compactGlossaries": false, 10797 10698 "uniqueExpressions": [ 10798 10699 "打ち込む" ··· 11214 11115 "compactTags": false, 11215 11116 "group": false, 11216 11117 "merge": false, 11217 - "modeTermKanji": false, 11218 - "modeTermKana": false, 11219 - "modeKanji": false, 11220 11118 "compactGlossaries": false, 11221 11119 "uniqueExpressions": [ 11222 11120 "打ち込む" ··· 11570 11468 "compactTags": false, 11571 11469 "group": false, 11572 11470 "merge": false, 11573 - "modeTermKanji": false, 11574 - "modeTermKana": false, 11575 - "modeKanji": false, 11576 11471 "compactGlossaries": false, 11577 11472 "uniqueExpressions": [ 11578 11473 "打つ" ··· 11891 11786 "compactTags": false, 11892 11787 "group": false, 11893 11788 "merge": false, 11894 - "modeTermKanji": false, 11895 - "modeTermKana": false, 11896 - "modeKanji": false, 11897 11789 "compactGlossaries": false, 11898 11790 "uniqueExpressions": [ 11899 11791 "打つ" ··· 12055 11947 "compactTags": false, 12056 11948 "group": false, 12057 11949 "merge": false, 12058 - "modeTermKanji": false, 12059 - "modeTermKana": false, 12060 - "modeKanji": false, 12061 11950 "compactGlossaries": false, 12062 11951 "uniqueExpressions": [ 12063 11952 "画像" ··· 12488 12377 "compactTags": false, 12489 12378 "group": true, 12490 12379 "merge": false, 12491 - "modeTermKanji": false, 12492 - "modeTermKana": false, 12493 - "modeKanji": false, 12494 12380 "compactGlossaries": false, 12495 12381 "uniqueExpressions": [ 12496 12382 "打ち込む" ··· 12939 12825 "compactTags": false, 12940 12826 "group": true, 12941 12827 "merge": false, 12942 - "modeTermKanji": false, 12943 - "modeTermKana": false, 12944 - "modeKanji": false, 12945 12828 "compactGlossaries": false, 12946 12829 "uniqueExpressions": [ 12947 12830 "打ち込む" ··· 13330 13213 "compactTags": false, 13331 13214 "group": true, 13332 13215 "merge": false, 13333 - "modeTermKanji": false, 13334 - "modeTermKana": false, 13335 - "modeKanji": false, 13336 13216 "compactGlossaries": false, 13337 13217 "uniqueExpressions": [ 13338 13218 "打つ" ··· 13686 13566 "compactTags": false, 13687 13567 "group": true, 13688 13568 "merge": false, 13689 - "modeTermKanji": false, 13690 - "modeTermKana": false, 13691 - "modeKanji": false, 13692 13569 "compactGlossaries": false, 13693 13570 "uniqueExpressions": [ 13694 13571 "打つ" ··· 14001 13878 "compactTags": false, 14002 13879 "group": true, 14003 13880 "merge": false, 14004 - "modeTermKanji": false, 14005 - "modeTermKana": false, 14006 - "modeKanji": false, 14007 13881 "compactGlossaries": false, 14008 13882 "uniqueExpressions": [ 14009 13883 "打" ··· 14325 14199 "compactTags": false, 14326 14200 "group": true, 14327 14201 "merge": false, 14328 - "modeTermKanji": false, 14329 - "modeTermKana": false, 14330 - "modeKanji": false, 14331 14202 "compactGlossaries": false, 14332 14203 "uniqueExpressions": [ 14333 14204 "打" ··· 15047 14918 "compactTags": false, 15048 14919 "group": false, 15049 14920 "merge": true, 15050 - "modeTermKanji": false, 15051 - "modeTermKana": false, 15052 - "modeKanji": false, 15053 14921 "compactGlossaries": false, 15054 14922 "uniqueExpressions": [ 15055 14923 "打ち込む" ··· 15709 15577 "compactTags": false, 15710 15578 "group": false, 15711 15579 "merge": true, 15712 - "modeTermKanji": false, 15713 - "modeTermKana": false, 15714 - "modeKanji": false, 15715 15580 "compactGlossaries": false, 15716 15581 "uniqueExpressions": [ 15717 15582 "打つ" ··· 16018 15883 "compactTags": false, 16019 15884 "group": false, 16020 15885 "merge": true, 16021 - "modeTermKanji": false, 16022 - "modeTermKana": false, 16023 - "modeKanji": false, 16024 15886 "compactGlossaries": false, 16025 15887 "uniqueExpressions": [ 16026 15888 "打" ··· 16335 16197 "compactTags": false, 16336 16198 "group": false, 16337 16199 "merge": true, 16338 - "modeTermKanji": false, 16339 - "modeTermKana": false, 16340 - "modeKanji": false, 16341 16200 "compactGlossaries": false, 16342 16201 "uniqueExpressions": [ 16343 16202 "打" ··· 16750 16609 "compactTags": false, 16751 16610 "group": false, 16752 16611 "merge": false, 16753 - "modeTermKanji": false, 16754 - "modeTermKana": false, 16755 - "modeKanji": false, 16756 16612 "compactGlossaries": false, 16757 16613 "uniqueExpressions": [ 16758 16614 "打ち込む" ··· 17195 17051 "compactTags": false, 17196 17052 "group": false, 17197 17053 "merge": false, 17198 - "modeTermKanji": false, 17199 - "modeTermKana": false, 17200 - "modeKanji": false, 17201 17054 "compactGlossaries": false, 17202 17055 "uniqueExpressions": [ 17203 17056 "打ち込む" ··· 17640 17493 "compactTags": false, 17641 17494 "group": false, 17642 17495 "merge": false, 17643 - "modeTermKanji": false, 17644 - "modeTermKana": false, 17645 - "modeKanji": false, 17646 17496 "compactGlossaries": false, 17647 17497 "uniqueExpressions": [ 17648 17498 "打ち込む" ··· 18085 17935 "compactTags": false, 18086 17936 "group": false, 18087 17937 "merge": false, 18088 - "modeTermKanji": false, 18089 - "modeTermKana": false, 18090 - "modeKanji": false, 18091 17938 "compactGlossaries": false, 18092 17939 "uniqueExpressions": [ 18093 17940 "打ち込む" ··· 18441 18288 "compactTags": false, 18442 18289 "group": false, 18443 18290 "merge": false, 18444 - "modeTermKanji": false, 18445 - "modeTermKana": false, 18446 - "modeKanji": false, 18447 18291 "compactGlossaries": false, 18448 18292 "uniqueExpressions": [ 18449 18293 "打つ" ··· 18762 18606 "compactTags": false, 18763 18607 "group": false, 18764 18608 "merge": false, 18765 - "modeTermKanji": false, 18766 - "modeTermKana": false, 18767 - "modeKanji": false, 18768 18609 "compactGlossaries": false, 18769 18610 "uniqueExpressions": [ 18770 18611 "打つ" ··· 19083 18924 "compactTags": false, 19084 18925 "group": false, 19085 18926 "merge": false, 19086 - "modeTermKanji": false, 19087 - "modeTermKana": false, 19088 - "modeKanji": false, 19089 18927 "compactGlossaries": false, 19090 18928 "uniqueExpressions": [ 19091 18929 "打つ" ··· 19404 19242 "compactTags": false, 19405 19243 "group": false, 19406 19244 "merge": false, 19407 - "modeTermKanji": false, 19408 - "modeTermKana": false, 19409 - "modeKanji": false, 19410 19245 "compactGlossaries": false, 19411 19246 "uniqueExpressions": [ 19412 19247 "打つ" ··· 19720 19555 "compactTags": false, 19721 19556 "group": false, 19722 19557 "merge": false, 19723 - "modeTermKanji": false, 19724 - "modeTermKana": false, 19725 - "modeKanji": false, 19726 19558 "compactGlossaries": false, 19727 19559 "uniqueExpressions": [ 19728 19560 "打" ··· 20045 19877 "compactTags": false, 20046 19878 "group": false, 20047 19879 "merge": false, 20048 - "modeTermKanji": false, 20049 - "modeTermKana": false, 20050 - "modeKanji": false, 20051 19880 "compactGlossaries": false, 20052 19881 "uniqueExpressions": [ 20053 19882 "打" ··· 20439 20268 "compactTags": false, 20440 20269 "group": false, 20441 20270 "merge": false, 20442 - "modeTermKanji": false, 20443 - "modeTermKana": false, 20444 - "modeKanji": false, 20445 20271 "compactGlossaries": false, 20446 20272 "uniqueExpressions": [ 20447 20273 "打ち込む" ··· 20863 20689 "compactTags": false, 20864 20690 "group": false, 20865 20691 "merge": false, 20866 - "modeTermKanji": false, 20867 - "modeTermKana": false, 20868 - "modeKanji": false, 20869 20692 "compactGlossaries": false, 20870 20693 "uniqueExpressions": [ 20871 20694 "打ち込む" ··· 21287 21110 "compactTags": false, 21288 21111 "group": false, 21289 21112 "merge": false, 21290 - "modeTermKanji": false, 21291 - "modeTermKana": false, 21292 - "modeKanji": false, 21293 21113 "compactGlossaries": false, 21294 21114 "uniqueExpressions": [ 21295 21115 "打ち込む" ··· 21711 21531 "compactTags": false, 21712 21532 "group": false, 21713 21533 "merge": false, 21714 - "modeTermKanji": false, 21715 - "modeTermKana": false, 21716 - "modeKanji": false, 21717 21534 "compactGlossaries": false, 21718 21535 "uniqueExpressions": [ 21719 21536 "打ち込む" ··· 22067 21884 "compactTags": false, 22068 21885 "group": false, 22069 21886 "merge": false, 22070 - "modeTermKanji": false, 22071 - "modeTermKana": false, 22072 - "modeKanji": false, 22073 21887 "compactGlossaries": false, 22074 21888 "uniqueExpressions": [ 22075 21889 "打つ" ··· 22388 22202 "compactTags": false, 22389 22203 "group": false, 22390 22204 "merge": false, 22391 - "modeTermKanji": false, 22392 - "modeTermKana": false, 22393 - "modeKanji": false, 22394 22205 "compactGlossaries": false, 22395 22206 "uniqueExpressions": [ 22396 22207 "打つ" ··· 22709 22520 "compactTags": false, 22710 22521 "group": false, 22711 22522 "merge": false, 22712 - "modeTermKanji": false, 22713 - "modeTermKana": false, 22714 - "modeKanji": false, 22715 22523 "compactGlossaries": false, 22716 22524 "uniqueExpressions": [ 22717 22525 "打つ" ··· 23030 22838 "compactTags": false, 23031 22839 "group": false, 23032 22840 "merge": false, 23033 - "modeTermKanji": false, 23034 - "modeTermKana": false, 23035 - "modeKanji": false, 23036 22841 "compactGlossaries": false, 23037 22842 "uniqueExpressions": [ 23038 22843 "打つ" ··· 23346 23151 "compactTags": false, 23347 23152 "group": false, 23348 23153 "merge": false, 23349 - "modeTermKanji": false, 23350 - "modeTermKana": false, 23351 - "modeKanji": false, 23352 23154 "compactGlossaries": false, 23353 23155 "uniqueExpressions": [ 23354 23156 "打" ··· 23671 23473 "compactTags": false, 23672 23474 "group": false, 23673 23475 "merge": false, 23674 - "modeTermKanji": false, 23675 - "modeTermKana": false, 23676 - "modeKanji": false, 23677 23476 "compactGlossaries": false, 23678 23477 "uniqueExpressions": [ 23679 23478 "打" ··· 24065 23864 "compactTags": false, 24066 23865 "group": false, 24067 23866 "merge": false, 24068 - "modeTermKanji": false, 24069 - "modeTermKana": false, 24070 - "modeKanji": false, 24071 23867 "compactGlossaries": false, 24072 23868 "uniqueExpressions": [ 24073 23869 "打ち込む" ··· 24489 24285 "compactTags": false, 24490 24286 "group": false, 24491 24287 "merge": false, 24492 - "modeTermKanji": false, 24493 - "modeTermKana": false, 24494 - "modeKanji": false, 24495 24288 "compactGlossaries": false, 24496 24289 "uniqueExpressions": [ 24497 24290 "打ち込む" ··· 24913 24706 "compactTags": false, 24914 24707 "group": false, 24915 24708 "merge": false, 24916 - "modeTermKanji": false, 24917 - "modeTermKana": false, 24918 - "modeKanji": false, 24919 24709 "compactGlossaries": false, 24920 24710 "uniqueExpressions": [ 24921 24711 "打ち込む" ··· 25337 25127 "compactTags": false, 25338 25128 "group": false, 25339 25129 "merge": false, 25340 - "modeTermKanji": false, 25341 - "modeTermKana": false, 25342 - "modeKanji": false, 25343 25130 "compactGlossaries": false, 25344 25131 "uniqueExpressions": [ 25345 25132 "打ち込む" ··· 25693 25480 "compactTags": false, 25694 25481 "group": false, 25695 25482 "merge": false, 25696 - "modeTermKanji": false, 25697 - "modeTermKana": false, 25698 - "modeKanji": false, 25699 25483 "compactGlossaries": false, 25700 25484 "uniqueExpressions": [ 25701 25485 "打つ" ··· 26014 25798 "compactTags": false, 26015 25799 "group": false, 26016 25800 "merge": false, 26017 - "modeTermKanji": false, 26018 - "modeTermKana": false, 26019 - "modeKanji": false, 26020 25801 "compactGlossaries": false, 26021 25802 "uniqueExpressions": [ 26022 25803 "打つ" ··· 26335 26116 "compactTags": false, 26336 26117 "group": false, 26337 26118 "merge": false, 26338 - "modeTermKanji": false, 26339 - "modeTermKana": false, 26340 - "modeKanji": false, 26341 26119 "compactGlossaries": false, 26342 26120 "uniqueExpressions": [ 26343 26121 "打つ" ··· 26656 26434 "compactTags": false, 26657 26435 "group": false, 26658 26436 "merge": false, 26659 - "modeTermKanji": false, 26660 - "modeTermKana": false, 26661 - "modeKanji": false, 26662 26437 "compactGlossaries": false, 26663 26438 "uniqueExpressions": [ 26664 26439 "打つ" ··· 26972 26747 "compactTags": false, 26973 26748 "group": false, 26974 26749 "merge": false, 26975 - "modeTermKanji": false, 26976 - "modeTermKana": false, 26977 - "modeKanji": false, 26978 26750 "compactGlossaries": false, 26979 26751 "uniqueExpressions": [ 26980 26752 "打" ··· 27297 27069 "compactTags": false, 27298 27070 "group": false, 27299 27071 "merge": false, 27300 - "modeTermKanji": false, 27301 - "modeTermKana": false, 27302 - "modeKanji": false, 27303 27072 "compactGlossaries": false, 27304 27073 "uniqueExpressions": [ 27305 27074 "打" ··· 27464 27233 "compactTags": false, 27465 27234 "group": false, 27466 27235 "merge": false, 27467 - "modeTermKanji": false, 27468 - "modeTermKana": false, 27469 - "modeKanji": false, 27470 27236 "compactGlossaries": false, 27471 27237 "uniqueExpressions": [ 27472 27238 "読む" ··· 27626 27392 "compactTags": false, 27627 27393 "group": false, 27628 27394 "merge": false, 27629 - "modeTermKanji": false, 27630 - "modeTermKana": false, 27631 - "modeKanji": false, 27632 27395 "compactGlossaries": false, 27633 27396 "uniqueExpressions": [ 27634 27397 "強み" ··· 27797 27560 "compactTags": false, 27798 27561 "group": false, 27799 27562 "merge": false, 27800 - "modeTermKanji": false, 27801 - "modeTermKana": false, 27802 - "modeKanji": false, 27803 27563 "compactGlossaries": false, 27804 27564 "uniqueExpressions": [ 27805 27565 "読む" ··· 28519 28279 "compactTags": false, 28520 28280 "group": false, 28521 28281 "merge": true, 28522 - "modeTermKanji": false, 28523 - "modeTermKana": false, 28524 - "modeKanji": false, 28525 28282 "compactGlossaries": false, 28526 28283 "uniqueExpressions": [ 28527 28284 "打ち込む" ··· 29181 28938 "compactTags": false, 29182 28939 "group": false, 29183 28940 "merge": true, 29184 - "modeTermKanji": false, 29185 - "modeTermKana": false, 29186 - "modeKanji": false, 29187 28941 "compactGlossaries": false, 29188 28942 "uniqueExpressions": [ 29189 28943 "打つ" ··· 29431 29185 "compactTags": false, 29432 29186 "group": false, 29433 29187 "merge": false, 29434 - "modeTermKanji": false, 29435 - "modeTermKana": false, 29436 - "modeKanji": false, 29437 29188 "compactGlossaries": false, 29438 29189 "uniqueExpressions": [ 29439 29190 "お手前" ··· 29685 29436 "compactTags": false, 29686 29437 "group": false, 29687 29438 "merge": false, 29688 - "modeTermKanji": false, 29689 - "modeTermKana": false, 29690 - "modeKanji": false, 29691 29439 "compactGlossaries": false, 29692 29440 "uniqueExpressions": [ 29693 29441 "番号" ··· 29875 29623 "compactTags": false, 29876 29624 "group": false, 29877 29625 "merge": false, 29878 - "modeTermKanji": false, 29879 - "modeTermKana": false, 29880 - "modeKanji": false, 29881 29626 "compactGlossaries": false, 29882 29627 "uniqueExpressions": [ 29883 29628 "中腰" ··· 30065 29810 "compactTags": false, 30066 29811 "group": false, 30067 29812 "merge": false, 30068 - "modeTermKanji": false, 30069 - "modeTermKana": false, 30070 - "modeKanji": false, 30071 29813 "compactGlossaries": false, 30072 29814 "uniqueExpressions": [ 30073 29815 "所業" ··· 30255 29997 "compactTags": false, 30256 29998 "group": false, 30257 29999 "merge": false, 30258 - "modeTermKanji": false, 30259 - "modeTermKana": false, 30260 - "modeKanji": false, 30261 30000 "compactGlossaries": false, 30262 30001 "uniqueExpressions": [ 30263 30002 "土木工事" ··· 30465 30204 "compactTags": false, 30466 30205 "group": false, 30467 30206 "merge": false, 30468 - "modeTermKanji": false, 30469 - "modeTermKana": false, 30470 - "modeKanji": false, 30471 30207 "compactGlossaries": false, 30472 30208 "uniqueExpressions": [ 30473 30209 "好き" ··· 30660 30396 "compactTags": false, 30661 30397 "group": false, 30662 30398 "merge": false, 30663 - "modeTermKanji": false, 30664 - "modeTermKana": false, 30665 - "modeKanji": false, 30666 30399 "compactGlossaries": false, 30667 30400 "uniqueExpressions": [ 30668 30401 "構造" ··· 30802 30535 "compactTags": false, 30803 30536 "group": false, 30804 30537 "merge": false, 30805 - "modeTermKanji": false, 30806 - "modeTermKana": false, 30807 - "modeKanji": false, 30808 30538 "compactGlossaries": false, 30809 30539 "uniqueExpressions": [ 30810 30540 "のたまう" ··· 30906 30636 "compactTags": false, 30907 30637 "group": false, 30908 30638 "merge": false, 30909 - "modeTermKanji": false, 30910 - "modeTermKana": false, 30911 - "modeKanji": false, 30912 30639 "compactGlossaries": false, 30913 30640 "uniqueExpressions": [ 30914 30641 "39" ··· 31022 30749 "compactTags": false, 31023 30750 "group": false, 31024 30751 "merge": false, 31025 - "modeTermKanji": false, 31026 - "modeTermKana": false, 31027 - "modeKanji": false, 31028 30752 "compactGlossaries": false, 31029 30753 "uniqueExpressions": [ 31030 30754 "English" ··· 31138 30862 "compactTags": false, 31139 30863 "group": false, 31140 30864 "merge": false, 31141 - "modeTermKanji": false, 31142 - "modeTermKana": false, 31143 - "modeKanji": false, 31144 30865 "compactGlossaries": false, 31145 30866 "uniqueExpressions": [ 31146 30867 "USB" ··· 31459 31180 "compactTags": false, 31460 31181 "group": false, 31461 31182 "merge": false, 31462 - "modeTermKanji": false, 31463 - "modeTermKana": false, 31464 - "modeKanji": false, 31465 31183 "compactGlossaries": false, 31466 31184 "uniqueExpressions": [ 31467 31185 "打つ" ··· 31775 31493 "compactTags": false, 31776 31494 "group": false, 31777 31495 "merge": false, 31778 - "modeTermKanji": false, 31779 - "modeTermKana": false, 31780 - "modeKanji": false, 31781 31496 "compactGlossaries": false, 31782 31497 "uniqueExpressions": [ 31783 31498 "打つ" ··· 32096 31811 "compactTags": false, 32097 31812 "group": false, 32098 31813 "merge": false, 32099 - "modeTermKanji": false, 32100 - "modeTermKana": false, 32101 - "modeKanji": false, 32102 31814 "compactGlossaries": false, 32103 31815 "uniqueExpressions": [ 32104 31816 "打つ" ··· 32412 32124 "compactTags": false, 32413 32125 "group": false, 32414 32126 "merge": false, 32415 - "modeTermKanji": false, 32416 - "modeTermKana": false, 32417 - "modeKanji": false, 32418 32127 "compactGlossaries": false, 32419 32128 "uniqueExpressions": [ 32420 32129 "打つ" ··· 32567 32276 "compactTags": false, 32568 32277 "group": false, 32569 32278 "merge": false, 32570 - "modeTermKanji": false, 32571 - "modeTermKana": false, 32572 - "modeKanji": false, 32573 32279 "compactGlossaries": false, 32574 32280 "uniqueExpressions": [ 32575 32281 "テキスト" ··· 32888 32594 "compactTags": false, 32889 32595 "group": false, 32890 32596 "merge": false, 32891 - "modeTermKanji": false, 32892 - "modeTermKana": false, 32893 - "modeKanji": false, 32894 32597 "compactGlossaries": false, 32895 32598 "uniqueExpressions": [ 32896 32599 "打つ" ··· 33204 32907 "compactTags": false, 33205 32908 "group": false, 33206 32909 "merge": false, 33207 - "modeTermKanji": false, 33208 - "modeTermKana": false, 33209 - "modeKanji": false, 33210 32910 "compactGlossaries": false, 33211 32911 "uniqueExpressions": [ 33212 32912 "打つ" ··· 33341 33041 "compactTags": false, 33342 33042 "group": false, 33343 33043 "merge": false, 33344 - "modeTermKanji": false, 33345 - "modeTermKana": false, 33346 - "modeKanji": false, 33347 33044 "compactGlossaries": false, 33348 33045 "uniqueExpressions": [ 33349 33046 "凄い" ··· 33457 33154 "compactTags": false, 33458 33155 "group": false, 33459 33156 "merge": false, 33460 - "modeTermKanji": false, 33461 - "modeTermKana": false, 33462 - "modeKanji": false, 33463 33157 "compactGlossaries": false, 33464 33158 "uniqueExpressions": [ 33465 33159 "English" ··· 33573 33267 "compactTags": false, 33574 33268 "group": false, 33575 33269 "merge": false, 33576 - "modeTermKanji": false, 33577 - "modeTermKana": false, 33578 - "modeKanji": false, 33579 33270 "compactGlossaries": false, 33580 33271 "uniqueExpressions": [ 33581 33272 "language" ··· 33693 33384 "compactTags": false, 33694 33385 "group": false, 33695 33386 "merge": false, 33696 - "modeTermKanji": false, 33697 - "modeTermKana": false, 33698 - "modeKanji": false, 33699 33387 "compactGlossaries": false, 33700 33388 "uniqueExpressions": [ 33701 33389 "마시다" ··· 33809 33497 "compactTags": false, 33810 33498 "group": false, 33811 33499 "merge": false, 33812 - "modeTermKanji": false, 33813 - "modeTermKana": false, 33814 - "modeKanji": false, 33815 33500 "compactGlossaries": false, 33816 33501 "uniqueExpressions": [ 33817 33502 "English" ··· 33925 33610 "compactTags": false, 33926 33611 "group": false, 33927 33612 "merge": false, 33928 - "modeTermKanji": false, 33929 - "modeTermKana": false, 33930 - "modeKanji": false, 33931 33613 "compactGlossaries": false, 33932 33614 "uniqueExpressions": [ 33933 33615 "自重" ··· 34036 33718 "compactTags": false, 34037 33719 "group": false, 34038 33720 "merge": false, 34039 - "modeTermKanji": false, 34040 - "modeTermKana": false, 34041 - "modeKanji": false, 34042 33721 "compactGlossaries": false, 34043 33722 "uniqueExpressions": [ 34044 33723 "自重" ··· 34152 33831 "compactTags": false, 34153 33832 "group": false, 34154 33833 "merge": false, 34155 - "modeTermKanji": false, 34156 - "modeTermKana": false, 34157 - "modeKanji": false, 34158 33834 "compactGlossaries": false, 34159 33835 "uniqueExpressions": [ 34160 33836 "自重" ··· 34263 33939 "compactTags": false, 34264 33940 "group": false, 34265 33941 "merge": false, 34266 - "modeTermKanji": false, 34267 - "modeTermKana": false, 34268 - "modeKanji": false, 34269 33942 "compactGlossaries": false, 34270 33943 "uniqueExpressions": [ 34271 33944 "自重"
+14 -9
test/options-util.test.js
··· 496 496 server: 'http://127.0.0.1:8765', 497 497 tags: ['yomitan'], 498 498 screenshot: {format: 'png', quality: 92}, 499 - terms: { 499 + cardFormats: [{ 500 + type: 'term', 501 + name: 'Expression', 502 + icon: 'big-circle', 500 503 deck: '', 501 504 model: '', 502 505 fields: { ··· 505 508 value: '{popup-selection-text}', 506 509 }, 507 510 }, 508 - }, 509 - kanji: { 511 + }, { 512 + type: 'kanji', 513 + name: 'Kanji', 514 + icon: 'big-circle', 510 515 deck: '', 511 516 model: '', 512 517 fields: { ··· 515 520 value: '{popup-selection-text}', 516 521 }, 517 522 }, 518 - }, 523 + }], 519 524 duplicateBehavior: 'new', 520 525 duplicateScope: 'collection', 521 526 duplicateScopeCheckAllModels: false, ··· 562 567 {action: 'nextEntry', argument: '1', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 563 568 {action: 'historyBackward', argument: '', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 564 569 {action: 'historyForward', argument: '', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 565 - {action: 'addNoteKanji', argument: '', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 566 - {action: 'addNoteTermKanji', argument: '', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 567 - {action: 'addNoteTermKana', argument: '', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 570 + {action: 'addNote', argument: '1', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 571 + {action: 'addNote', argument: '0', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 572 + {action: 'addNote', argument: '1', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 568 573 {action: 'playAudio', argument: '', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 569 - {action: 'viewNotes', argument: '', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 574 + {action: 'viewNotes', argument: '0', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, 570 575 {action: 'copyHostSelection', argument: '', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true}, 571 576 {action: 'profilePrevious', argument: '', key: 'Minus', modifiers: ['alt'], scopes: ['popup', 'search', 'web'], enabled: true}, 572 577 {action: 'profileNext', argument: '', key: 'Equal', modifiers: ['alt'], scopes: ['popup', 'search', 'web'], enabled: true}, ··· 682 687 }, 683 688 ], 684 689 profileCurrent: 0, 685 - version: 63, 690 + version: 64, 686 691 global: { 687 692 database: { 688 693 prefixWildcardsSupported: false,
+2 -2
test/playwright/integration.spec.js
··· 87 87 await page.locator('select.anki-card-model').selectOption('Mock Model'); 88 88 const mockFields = getMockModelFields(); 89 89 for (const [modelField, value] of mockFields) { 90 - await page.locator(`[data-setting="anki.terms.fields.${modelField}.value"]`).fill(value); 90 + await page.locator(`[data-setting="anki.notes[0].fields.${modelField}.value"]`).fill(value); 91 91 } 92 92 await page.locator('#anki-cards-modal > div > div.modal-footer > button:nth-child(2)').click(); 93 93 await writeToClipboardFromPage(page, '読むの例文'); ··· 100 100 await expect(page.locator('#search-textbox')).toHaveValue('読む'); 101 101 }).toPass({timeout: 5000}); 102 102 await page.locator('#search-textbox').press('Enter'); 103 - await page.locator('[data-mode="term-kanji"]').click(); 103 + await page.locator('[data-action="save-note"][data-card-format-index="0"]').click(); 104 104 const addNoteReqBody = await addNotePromiseDetails.promise; 105 105 expect(addNoteReqBody).toMatchObject(getExpectedAddNoteBody()); 106 106 });
+20 -9
test/utilities/anki.js
··· 22 22 23 23 /** 24 24 * @param {import('dictionary').DictionaryEntryType} type 25 - * @returns {import('anki-note-builder').Field[]} 25 + * @returns {import('settings').AnkiFields} 26 26 */ 27 27 function createTestFields(type) { 28 - /** @type {import('anki-note-builder').Field[]} */ 29 - const fields = []; 28 + /** @type {import('settings').AnkiFields} */ 29 + const fields = {}; 30 30 for (const marker of getStandardFieldMarkers(type)) { 31 - fields.push([marker, {value: `{${marker}}`, overwriteMode: 'coalesce'}]); 31 + fields[marker] = {value: `{${marker}}`, overwriteMode: 'coalesce'}; 32 32 } 33 33 return fields; 34 34 } ··· 51 51 const data = { 52 52 dictionaryEntry, 53 53 resultOutputMode: mode, 54 - mode: 'test', 54 + cardFormat: { 55 + type: 'term', 56 + name: 'test', 57 + deck: 'deck', 58 + model: 'model', 59 + fields: {}, 60 + icon: 'big-circle', 61 + }, 55 62 glossaryLayoutMode: 'default', 56 63 compactTags: false, 57 64 context: { ··· 113 120 /** @type {import('anki-note-builder').CreateNoteDetails} */ 114 121 const details = { 115 122 dictionaryEntry, 116 - mode: 'test', 123 + cardFormat: { 124 + type: dictionaryEntry.type, 125 + name: 'test', 126 + deck: 'deckName', 127 + model: 'modelName', 128 + fields: createTestFields(dictionaryEntry.type), 129 + icon: 'big-circle', 130 + }, 117 131 context, 118 132 template, 119 - deckName: 'deckName', 120 - modelName: 'modelName', 121 - fields: createTestFields(dictionaryEntry.type), 122 133 tags: ['yomitan'], 123 134 duplicateScope: 'collection', 124 135 duplicateScopeCheckAllModels: false,
+2 -5
types/ext/anki-note-builder.d.ts
··· 28 28 29 29 export type CreateNoteDetails = { 30 30 dictionaryEntry: Dictionary.DictionaryEntry; 31 - mode: AnkiTemplatesInternal.CreateMode; 31 + cardFormat: Settings.AnkiCardFormat; 32 32 context: AnkiTemplatesInternal.Context; 33 33 template: string; 34 - deckName: string; 35 - modelName: string; 36 - fields: Field[]; 37 34 tags: string[]; 38 35 requirements: Requirement[]; 39 36 duplicateScope: Settings.AnkiDuplicateScope; ··· 61 58 62 59 export type GetRenderingDataDetails = { 63 60 dictionaryEntry: Dictionary.DictionaryEntry; 64 - mode: AnkiTemplatesInternal.CreateMode; 61 + cardFormat: Settings.AnkiCardFormat; 65 62 context: AnkiTemplatesInternal.Context; 66 63 resultOutputMode?: Settings.ResultOutputMode; 67 64 glossaryLayoutMode?: Settings.GlossaryLayoutMode;
+1 -9
types/ext/anki-templates-internal.d.ts
··· 32 32 offset?: number; 33 33 }; 34 34 35 - export type CreateModeNoTest = 'kanji' | 'term-kanji' | 'term-kana'; 36 - 37 - export type CreateMode = CreateModeNoTest | 'test'; 38 - 39 35 export type CreateDetails = { 40 - /** The dictionary entry. */ 41 36 dictionaryEntry: Dictionary.DictionaryEntry; 42 - /** The result output mode. */ 43 37 resultOutputMode: Settings.ResultOutputMode; 44 - /** The mode being used to generate the Anki data. */ 45 - mode: CreateMode; 46 - /** The glossary layout mode. */ 38 + cardFormat: Settings.AnkiCardFormat; 47 39 glossaryLayoutMode: Settings.GlossaryLayoutMode; 48 40 /** Whether or not compact tags mode is enabled. */ 49 41 compactTags: boolean;
-3
types/ext/anki-templates.d.ts
··· 69 69 compactTags: boolean; 70 70 group: boolean; 71 71 merge: boolean; 72 - modeTermKanji: boolean; 73 - modeTermKana: boolean; 74 - modeKanji: boolean; 75 72 compactGlossaries: boolean; 76 73 readonly uniqueExpressions: string[]; 77 74 readonly uniqueReadings: string[];
+5 -7
types/ext/display-anki.d.ts
··· 18 18 import type * as Anki from './anki'; 19 19 import type * as AnkiNoteBuilder from './anki-note-builder'; 20 20 import type * as AnkiTemplates from './anki-templates'; 21 - import type * as AnkiTemplatesInternal from './anki-templates-internal'; 22 - 23 - export type CreateMode = AnkiTemplatesInternal.CreateModeNoTest; 21 + import type * as Settings from './settings'; 24 22 25 23 export type LogData = { 26 24 ankiNoteData: AnkiTemplates.NoteData | undefined; ··· 29 27 }; 30 28 31 29 export type AnkiNoteLogData = { 32 - mode: CreateMode; 30 + cardFormatIndex: number; 33 31 note: Anki.Note | undefined; 34 32 errors?: Error[]; 35 33 requirements?: AnkiNoteBuilder.Requirement[]; 36 34 }; 37 35 38 36 export type DictionaryEntryDetails = { 39 - modeMap: Map<CreateMode, DictionaryEntryModeDetails>; 37 + noteMap: Map<number, DictionaryEntryNoteDetails>; 40 38 }; 41 39 42 - export type DictionaryEntryModeDetails = { 43 - mode: CreateMode; 40 + export type DictionaryEntryNoteDetails = { 41 + cardFormat: Settings.AnkiCardFormat; 44 42 note: Anki.Note; 45 43 errors: Error[]; 46 44 requirements: AnkiNoteBuilder.Requirement[];
+11 -7
types/ext/settings.d.ts
··· 292 292 server: string; 293 293 tags: string[]; 294 294 screenshot: AnkiScreenshotOptions; 295 - terms: AnkiNoteOptions; 296 - kanji: AnkiNoteOptions; 295 + cardFormats: AnkiCardFormat[]; 297 296 duplicateScope: AnkiDuplicateScope; 298 297 duplicateScopeCheckAllModels: boolean; 299 298 duplicateBehavior: AnkiDuplicateBehavior; ··· 311 310 quality: number; 312 311 }; 313 312 314 - export type AnkiNoteOptions = { 313 + export type AnkiCardFormat = { 314 + type: 'kanji' | 'term'; 315 + name: string; 315 316 deck: string; 316 317 model: string; 317 - fields: AnkiNoteFields; 318 + fields: AnkiFields; 319 + icon: AddNoteIcon; 318 320 }; 319 321 320 - export type AnkiNoteFields = { 321 - [key: string]: AnkiNoteField; 322 + export type AddNoteIcon = 'big-circle' | 'small-circle' | 'big-square' | 'big-diamond'; 323 + 324 + export type AnkiFields = { 325 + [key: string]: AnkiField; 322 326 }; 323 327 324 - export type AnkiNoteField = { 328 + export type AnkiField = { 325 329 value: string; 326 330 overwriteMode: AnkiNoteFieldOverwriteMode; 327 331 };