Merge pull request #49 from cybercase/asivery_work_on_groups_and_full_width_titles
Asivery's work on groups and full width titles
This commit is contained in:
commit
793422878d
|
@ -25,11 +25,13 @@
|
||||||
"@types/w3c-web-usb": "^1.0.4",
|
"@types/w3c-web-usb": "^1.0.4",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
|
"jconv": "^0.1.5",
|
||||||
"lint-staged": "^10.5.3",
|
"lint-staged": "^10.5.3",
|
||||||
"music-metadata-browser": "^2.2.0",
|
"music-metadata-browser": "^2.2.0",
|
||||||
"netmd-js": "^1.4.0",
|
"netmd-js": "^2.0.1",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-dropzone": "^10.2.2",
|
"react-dropzone": "^10.2.2",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
"async-mutex": "^0.2.6",
|
"async-mutex": "^0.2.6",
|
||||||
"gh-pages": "^2.2.0"
|
"gh-pages": "^2.2.0"
|
||||||
}
|
}
|
||||||
|
@ -1033,11 +1036,6 @@
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime-corejs3/node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
},
|
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.8.3",
|
"version": "7.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz",
|
||||||
|
@ -1837,11 +1835,6 @@
|
||||||
"node": ">= 8.3"
|
"node": ">= 8.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@testing-library/dom/node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
},
|
|
||||||
"node_modules/@testing-library/dom/node_modules/supports-color": {
|
"node_modules/@testing-library/dom/node_modules/supports-color": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||||
|
@ -2057,6 +2050,15 @@
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-beautiful-dnd": {
|
||||||
|
"version": "13.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
||||||
|
"integrity": "sha512-1lBBxVSutE8CQM37Jq7KvJwuA94qaEEqsx+G0dnwzG6Sfwf6JGcNeFk5jjjhJli1q2naeMZm+D/dvT/zyX4QPw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "16.9.10",
|
"version": "16.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz",
|
||||||
|
@ -4669,6 +4671,14 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-box-model": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-color-keywords": {
|
"node_modules/css-color-keywords": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
@ -5391,11 +5401,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||||
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
|
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
|
||||||
},
|
},
|
||||||
"node_modules/dom-helpers/node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
},
|
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
|
||||||
|
@ -11384,6 +11389,11 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||||
|
},
|
||||||
"node_modules/memory-fs": {
|
"node_modules/memory-fs": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||||
|
@ -11987,9 +11997,9 @@
|
||||||
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
|
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
|
||||||
},
|
},
|
||||||
"node_modules/netmd-js": {
|
"node_modules/netmd-js": {
|
||||||
"version": "1.4.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/netmd-js/-/netmd-js-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/netmd-js/-/netmd-js-2.0.1.tgz",
|
||||||
"integrity": "sha512-8OXEO4Gh6Me3XU2igHmVbk+hn06O3N5xlM/c38w97rNQDRpCAppO8jqq+fryaZd7QZwmIxXqwwY88KIehoieAg==",
|
"integrity": "sha512-9rMt+tcpqYM1/IyudyzVeOzZ+E55/+RvHf6NNOzQJbb0knySG5oaLich2NXSsFT45Wrg+c43uLMu3iOKmjW4aA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-bunyan": "^1.5.3",
|
"browser-bunyan": "^1.5.3",
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
|
@ -14528,6 +14538,11 @@
|
||||||
"performance-now": "^2.1.0"
|
"performance-now": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||||
|
},
|
||||||
"node_modules/randombytes": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
@ -14626,6 +14641,35 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-beautiful-dnd": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.9.2",
|
||||||
|
"css-box-model": "^1.2.0",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
|
"raf-schd": "^4.0.2",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"use-memo-one": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.5 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.5 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-beautiful-dnd/node_modules/@babel/runtime": {
|
||||||
|
"version": "7.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
|
||||||
|
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dev-utils": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.1.0.tgz",
|
||||||
|
@ -14901,11 +14945,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
"node_modules/react-redux/node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
},
|
|
||||||
"node_modules/react-scripts": {
|
"node_modules/react-scripts": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.3.1.tgz",
|
||||||
|
@ -15231,9 +15270,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regenerator-runtime": {
|
"node_modules/regenerator-runtime": {
|
||||||
"version": "0.13.3",
|
"version": "0.13.7",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||||
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
|
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||||
},
|
},
|
||||||
"node_modules/regenerator-transform": {
|
"node_modules/regenerator-transform": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
|
@ -17310,6 +17349,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||||
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
|
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
|
||||||
|
},
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
@ -17771,6 +17815,14 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-memo-one": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util": {
|
"node_modules/util": {
|
||||||
"version": "0.10.3",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||||
|
@ -20049,13 +20101,6 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-js-pure": "^3.0.0",
|
"core-js-pure": "^3.0.0",
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/template": {
|
"@babel/template": {
|
||||||
|
@ -20726,11 +20771,6 @@
|
||||||
"react-is": "^16.12.0"
|
"react-is": "^16.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
},
|
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||||
|
@ -20947,6 +20987,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-beautiful-dnd": {
|
||||||
|
"version": "13.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
||||||
|
"integrity": "sha512-1lBBxVSutE8CQM37Jq7KvJwuA94qaEEqsx+G0dnwzG6Sfwf6JGcNeFk5jjjhJli1q2naeMZm+D/dvT/zyX4QPw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-dom": {
|
"@types/react-dom": {
|
||||||
"version": "16.9.10",
|
"version": "16.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz",
|
||||||
|
@ -23196,6 +23245,14 @@
|
||||||
"postcss": "^7.0.5"
|
"postcss": "^7.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"css-box-model": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||||
|
"requires": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"css-color-keywords": {
|
"css-color-keywords": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
@ -23784,11 +23841,6 @@
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||||
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
|
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
|
||||||
},
|
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -28615,6 +28667,11 @@
|
||||||
"p-is-promise": "^2.0.0"
|
"p-is-promise": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"memoize-one": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||||
|
},
|
||||||
"memory-fs": {
|
"memory-fs": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||||
|
@ -29118,9 +29175,9 @@
|
||||||
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
|
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
|
||||||
},
|
},
|
||||||
"netmd-js": {
|
"netmd-js": {
|
||||||
"version": "1.4.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/netmd-js/-/netmd-js-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/netmd-js/-/netmd-js-2.0.1.tgz",
|
||||||
"integrity": "sha512-8OXEO4Gh6Me3XU2igHmVbk+hn06O3N5xlM/c38w97rNQDRpCAppO8jqq+fryaZd7QZwmIxXqwwY88KIehoieAg==",
|
"integrity": "sha512-9rMt+tcpqYM1/IyudyzVeOzZ+E55/+RvHf6NNOzQJbb0knySG5oaLich2NXSsFT45Wrg+c43uLMu3iOKmjW4aA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"browser-bunyan": "^1.5.3",
|
"browser-bunyan": "^1.5.3",
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
|
@ -31214,6 +31271,11 @@
|
||||||
"performance-now": "^2.1.0"
|
"performance-now": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
@ -31295,6 +31357,30 @@
|
||||||
"whatwg-fetch": "^3.0.0"
|
"whatwg-fetch": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-beautiful-dnd": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.9.2",
|
||||||
|
"css-box-model": "^1.2.0",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
|
"raf-schd": "^4.0.2",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"use-memo-one": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.14.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
|
||||||
|
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dev-utils": {
|
"react-dev-utils": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.1.0.tgz",
|
||||||
|
@ -31502,11 +31588,6 @@
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.13.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -31775,9 +31856,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.3",
|
"version": "0.13.7",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||||
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
|
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||||
},
|
},
|
||||||
"regenerator-transform": {
|
"regenerator-transform": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
|
@ -33511,6 +33592,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||||
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
|
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
|
||||||
},
|
},
|
||||||
|
"tiny-invariant": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
|
||||||
|
},
|
||||||
"tiny-warning": {
|
"tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
@ -33884,6 +33970,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
||||||
},
|
},
|
||||||
|
"use-memo-one": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"util": {
|
"util": {
|
||||||
"version": "0.10.3",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||||
|
|
|
@ -21,11 +21,13 @@
|
||||||
"@types/w3c-web-usb": "^1.0.4",
|
"@types/w3c-web-usb": "^1.0.4",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
|
"jconv": "^0.1.5",
|
||||||
"lint-staged": "^10.5.3",
|
"lint-staged": "^10.5.3",
|
||||||
"music-metadata-browser": "^2.2.0",
|
"music-metadata-browser": "^2.2.0",
|
||||||
"netmd-js": "^1.4.0",
|
"netmd-js": "^2.0.1",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-dropzone": "^10.2.2",
|
"react-dropzone": "^10.2.2",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
|
@ -72,6 +74,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
"async-mutex": "^0.2.6",
|
"async-mutex": "^0.2.6",
|
||||||
"gh-pages": "^2.2.0"
|
"gh-pages": "^2.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,9 +142,10 @@ export const Controls = () => {
|
||||||
} else if (tracks.length === 0) {
|
} else if (tracks.length === 0) {
|
||||||
message = `BLANKDISC`;
|
message = `BLANKDISC`;
|
||||||
} else if (deviceStatus && deviceStatus.track !== null && tracks[deviceStatus.track]) {
|
} else if (deviceStatus && deviceStatus.track !== null && tracks[deviceStatus.track]) {
|
||||||
|
let title = tracks[deviceStatus.track].fullWidthTitle || tracks[deviceStatus.track].title;
|
||||||
message =
|
message =
|
||||||
(deviceStatus.track + 1).toString().padStart(3, '0') +
|
(deviceStatus.track + 1).toString().padStart(3, '0') +
|
||||||
(tracks[deviceStatus.track].title ? ' - ' + tracks[deviceStatus.track].title : '');
|
(title ? ' - ' + title : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lcdScroll, setLcdScroll] = useState(0);
|
const [lcdScroll, setLcdScroll] = useState(0);
|
||||||
|
|
|
@ -1,18 +1,37 @@
|
||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { listContent, deleteTracks, moveTrack } from '../redux/actions';
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Draggable,
|
||||||
|
DraggableProvided,
|
||||||
|
DropResult,
|
||||||
|
ResponderProvided,
|
||||||
|
Droppable,
|
||||||
|
DroppableProvided,
|
||||||
|
DroppableStateSnapshot,
|
||||||
|
} from 'react-beautiful-dnd';
|
||||||
|
import { listContent, deleteTracks, moveTrack, groupTracks, deleteGroup, dragDropTrack } from '../redux/actions';
|
||||||
import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
|
import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
|
||||||
import { actions as convertDialogActions } from '../redux/convert-dialog-feature';
|
import { actions as convertDialogActions } from '../redux/convert-dialog-feature';
|
||||||
import { actions as dumpDialogActions } from '../redux/dump-dialog-feature';
|
import { actions as dumpDialogActions } from '../redux/dump-dialog-feature';
|
||||||
|
|
||||||
import { formatTimeFromFrames, getTracks } from 'netmd-js';
|
import { formatTimeFromFrames, Track } from 'netmd-js';
|
||||||
import { control } from '../redux/actions';
|
import { control } from '../redux/actions';
|
||||||
|
|
||||||
import { belowDesktop, forAnyDesktop, getSortedTracks, useShallowEqualSelector } from '../utils';
|
import {
|
||||||
|
belowDesktop,
|
||||||
|
forAnyDesktop,
|
||||||
|
getGroupedTracks,
|
||||||
|
getSortedTracks,
|
||||||
|
isSequential,
|
||||||
|
useShallowEqualSelector,
|
||||||
|
EncodingName,
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
import { lighten, makeStyles } from '@material-ui/core/styles';
|
import { lighten, makeStyles } from '@material-ui/core/styles';
|
||||||
|
import { fade } from '@material-ui/core/styles/colorManipulator';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import Box from '@material-ui/core/Box';
|
import Box from '@material-ui/core/Box';
|
||||||
import Fab from '@material-ui/core/Fab';
|
import Fab from '@material-ui/core/Fab';
|
||||||
|
@ -22,6 +41,8 @@ import EditIcon from '@material-ui/icons/Edit';
|
||||||
import Backdrop from '@material-ui/core/Backdrop';
|
import Backdrop from '@material-ui/core/Backdrop';
|
||||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||||
import PauseIcon from '@material-ui/icons/Pause';
|
import PauseIcon from '@material-ui/icons/Pause';
|
||||||
|
import FolderIcon from '@material-ui/icons/Folder';
|
||||||
|
import CreateNewFolderIcon from '@material-ui/icons/CreateNewFolder';
|
||||||
|
|
||||||
import Table from '@material-ui/core/Table';
|
import Table from '@material-ui/core/Table';
|
||||||
import TableBody from '@material-ui/core/TableBody';
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
|
@ -46,9 +67,8 @@ import { TopMenu } from './topmenu';
|
||||||
import Checkbox from '@material-ui/core/Checkbox';
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
import * as BadgeImpl from '@material-ui/core/Badge/Badge';
|
import * as BadgeImpl from '@material-ui/core/Badge/Badge';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import Menu from '@material-ui/core/Menu';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
import { W95Main } from './win95/main';
|
import { W95Main } from './win95/main';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
add: {
|
add: {
|
||||||
|
@ -126,7 +146,7 @@ const useStyles = makeStyles(theme => ({
|
||||||
indexCell: {
|
indexCell: {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
width: `2ch`,
|
width: `16px`,
|
||||||
},
|
},
|
||||||
backdrop: {
|
backdrop: {
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
@ -138,7 +158,9 @@ const useStyles = makeStyles(theme => ({
|
||||||
},
|
},
|
||||||
controlButtonInTrackCommon: {
|
controlButtonInTrackCommon: {
|
||||||
width: `16px`,
|
width: `16px`,
|
||||||
|
height: `16px`,
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
|
cursor: 'pointer',
|
||||||
marginLeft: theme.spacing(-0.5),
|
marginLeft: theme.spacing(-0.5),
|
||||||
},
|
},
|
||||||
playButtonInTrackListPlaying: {
|
playButtonInTrackListPlaying: {
|
||||||
|
@ -153,15 +175,17 @@ const useStyles = makeStyles(theme => ({
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
},
|
},
|
||||||
playButtonInTrackListNotPlaying: {
|
playButtonInTrackListNotPlaying: {
|
||||||
visibility: 'hidden',
|
width: `0px`,
|
||||||
},
|
},
|
||||||
trackRow: {
|
trackRow: {
|
||||||
|
userSelect: 'none',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
/* For the tracks that aren't currently playing */
|
/* For the tracks that aren't currently playing */
|
||||||
'& $playButtonInTrackListNotPlaying': {
|
'& $playButtonInTrackListNotPlaying': {
|
||||||
visibility: 'visible',
|
width: `16px`,
|
||||||
},
|
},
|
||||||
'& $trackIndex': {
|
'& $trackIndex': {
|
||||||
|
width: `0px`,
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -174,8 +198,34 @@ const useStyles = makeStyles(theme => ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
inGroupTrackRow: {
|
||||||
|
'& > $indexCell': {
|
||||||
|
transform: `translateX(${theme.spacing(3)}px)`,
|
||||||
|
},
|
||||||
|
'& > $titleCell': {
|
||||||
|
transform: `translateX(${theme.spacing(3)}px)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deleteGroupButton: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
groupHeadRow: {
|
||||||
|
'&:hover': {
|
||||||
|
'& svg:not($deleteGroupButton)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'& $deleteGroupButton': {
|
||||||
|
display: 'inline-block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hoveringOverGroup: {
|
||||||
|
backgroundColor: `${fade(theme.palette.secondary.dark, 0.4)}`,
|
||||||
|
},
|
||||||
trackIndex: {
|
trackIndex: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
|
height: '16px',
|
||||||
|
width: '16px',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -187,6 +237,7 @@ export const Main = (props: {}) => {
|
||||||
const { vintageMode } = useShallowEqualSelector(state => state.appState);
|
const { vintageMode } = useShallowEqualSelector(state => state.appState);
|
||||||
|
|
||||||
const [selected, setSelected] = React.useState<number[]>([]);
|
const [selected, setSelected] = React.useState<number[]>([]);
|
||||||
|
const [lastClicked, setLastClicked] = useState(-1);
|
||||||
const selectedCount = selected.length;
|
const selectedCount = selected.length;
|
||||||
|
|
||||||
const [moveMenuAnchorEl, setMoveMenuAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [moveMenuAnchorEl, setMoveMenuAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
@ -207,6 +258,18 @@ export const Main = (props: {}) => {
|
||||||
[dispatch, selected, handleCloseMoveMenu]
|
[dispatch, selected, handleCloseMoveMenu]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
(result: DropResult, provided: ResponderProvided) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
let sourceList = parseInt(result.source.droppableId),
|
||||||
|
sourceIndex = result.source.index,
|
||||||
|
targetList = parseInt(result.destination.droppableId),
|
||||||
|
targetIndex = result.destination.index;
|
||||||
|
dispatch(dragDropTrack(sourceList, sourceIndex, targetList, targetIndex));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const handleShowDumpDialog = useCallback(() => {
|
const handleShowDumpDialog = useCallback(() => {
|
||||||
dispatch(dumpDialogActions.setVisible(true));
|
dispatch(dumpDialogActions.setVisible(true));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
@ -234,15 +297,28 @@ export const Main = (props: {}) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const tracks = getSortedTracks(disc);
|
const tracks = useMemo(() => getSortedTracks(disc), [disc]);
|
||||||
|
const groupedTracks = useMemo(() => getGroupedTracks(disc), [disc]);
|
||||||
|
|
||||||
// Action Handlers
|
// Action Handlers
|
||||||
const handleSelectClick = (event: React.MouseEvent, item: number) => {
|
const handleSelectClick = (event: React.MouseEvent, item: number) => {
|
||||||
if (selected.includes(item)) {
|
if (event.shiftKey && selected.length && lastClicked !== -1) {
|
||||||
|
let rangeBegin = Math.min(lastClicked + 1, item),
|
||||||
|
rangeEnd = Math.max(lastClicked - 1, item);
|
||||||
|
let copy = [...selected];
|
||||||
|
for (let i = rangeBegin; i <= rangeEnd; i++) {
|
||||||
|
let index = copy.indexOf(i);
|
||||||
|
if (index === -1) copy.push(i);
|
||||||
|
else copy.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (!copy.includes(item)) copy.push(item);
|
||||||
|
setSelected(copy);
|
||||||
|
} else if (selected.includes(item)) {
|
||||||
setSelected(selected.filter(i => i !== item));
|
setSelected(selected.filter(i => i !== item));
|
||||||
} else {
|
} else {
|
||||||
setSelected([...selected, item]);
|
setSelected([...selected, item]);
|
||||||
}
|
}
|
||||||
|
setLastClicked(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
@ -253,20 +329,31 @@ export const Main = (props: {}) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRenameDoubleClick = (event: React.MouseEvent, item: number) => {
|
const handleRenameDoubleClick = (event: React.MouseEvent, index: number, renameGroup?: boolean) => {
|
||||||
let selectedIndex = item;
|
let group, track;
|
||||||
let currentName = getTracks(disc!).find(track => track.index === selectedIndex)?.title ?? '';
|
if (renameGroup) {
|
||||||
|
group = groupedTracks.find(n => n.tracks[0]?.index === index);
|
||||||
|
if (!group) return;
|
||||||
|
} else {
|
||||||
|
track = tracks.find(n => n.index === index);
|
||||||
|
if (!track) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentName = (track ? track.title : group?.title) ?? '';
|
||||||
|
let currentFullWidthName = (track ? track.fullWidthTitle : group?.fullWidthTitle) ?? '';
|
||||||
dispatch(
|
dispatch(
|
||||||
batchActions([
|
batchActions([
|
||||||
renameDialogActions.setVisible(true),
|
renameDialogActions.setVisible(true),
|
||||||
|
renameDialogActions.setGroupIndex(group ? index : null),
|
||||||
renameDialogActions.setCurrentName(currentName),
|
renameDialogActions.setCurrentName(currentName),
|
||||||
renameDialogActions.setIndex(selectedIndex),
|
renameDialogActions.setCurrentFullWidthName(currentFullWidthName),
|
||||||
|
renameDialogActions.setIndex(track?.index ?? -1),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRenameActionClick = (event: React.MouseEvent) => {
|
const handleRenameActionClick = (event: React.MouseEvent) => {
|
||||||
|
if(event.detail !== 1) return; //Event retriggering when hitting enter in the dialog
|
||||||
handleRenameDoubleClick(event, selected[0]);
|
handleRenameDoubleClick(event, selected[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -274,6 +361,14 @@ export const Main = (props: {}) => {
|
||||||
dispatch(deleteTracks(selected));
|
dispatch(deleteTracks(selected));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGroupTracks = (event: React.MouseEvent) => {
|
||||||
|
dispatch(groupTracks(selected));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupRemoval = (event: React.MouseEvent, groupBegin: number) => {
|
||||||
|
dispatch(deleteGroup(groupBegin));
|
||||||
|
};
|
||||||
|
|
||||||
const handlePlayTrack = async (event: React.MouseEvent, track: number) => {
|
const handlePlayTrack = async (event: React.MouseEvent, track: number) => {
|
||||||
if (deviceStatus?.track !== track) {
|
if (deviceStatus?.track !== track) {
|
||||||
dispatch(control('goto', track));
|
dispatch(control('goto', track));
|
||||||
|
@ -289,6 +384,13 @@ export const Main = (props: {}) => {
|
||||||
} else dispatch(control('play'));
|
} else dispatch(control('play'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canGroup = useMemo(() => {
|
||||||
|
return (
|
||||||
|
tracks.filter(n => n.group === null && selected.includes(n.index)).length === selected.length &&
|
||||||
|
isSequential(selected.sort((a, b) => a - b))
|
||||||
|
);
|
||||||
|
}, [tracks, selected]);
|
||||||
|
|
||||||
if (vintageMode) {
|
if (vintageMode) {
|
||||||
const p = {
|
const p = {
|
||||||
disc,
|
disc,
|
||||||
|
@ -324,6 +426,64 @@ export const Main = (props: {}) => {
|
||||||
return <W95Main {...p} />;
|
return <W95Main {...p} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTrackRow = (track: Track, inGroup: boolean, additional: {}) => {
|
||||||
|
const child = (
|
||||||
|
<TableRow
|
||||||
|
{...additional}
|
||||||
|
hover
|
||||||
|
selected={selected.includes(track.index)}
|
||||||
|
onDoubleClick={event => handleRenameDoubleClick(event, track.index)}
|
||||||
|
onClick={event => handleSelectClick(event, track.index)}
|
||||||
|
className={clsx(classes.trackRow, { [classes.inGroupTrackRow]: inGroup })}
|
||||||
|
>
|
||||||
|
<TableCell className={classes.indexCell}>
|
||||||
|
{track.index === deviceStatus?.track && ['playing', 'paused'].includes(deviceStatus?.state) ? (
|
||||||
|
<span>
|
||||||
|
<PlayArrowIcon
|
||||||
|
className={clsx(classes.controlButtonInTrackCommon, classes.playButtonInTrackListPlaying, {
|
||||||
|
[classes.currentControlButton]: deviceStatus?.state === 'playing',
|
||||||
|
})}
|
||||||
|
onClick={event => {
|
||||||
|
handleCurrentClick(event);
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PauseIcon
|
||||||
|
className={clsx(classes.controlButtonInTrackCommon, classes.pauseButtonInTrackListPlaying, {
|
||||||
|
[classes.currentControlButton]: deviceStatus?.state === 'paused',
|
||||||
|
})}
|
||||||
|
onClick={event => {
|
||||||
|
handleCurrentClick(event);
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<span className={classes.trackIndex}>{track.index + 1}</span>
|
||||||
|
<PlayArrowIcon
|
||||||
|
className={clsx(classes.controlButtonInTrackCommon, classes.playButtonInTrackListNotPlaying)}
|
||||||
|
onClick={event => {
|
||||||
|
handlePlayTrack(event, track.index);
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={classes.titleCell} title={track.title ?? ''}>
|
||||||
|
{track.fullWidthTitle ? `${track.fullWidthTitle} / ` : ``}
|
||||||
|
{track.title || `No Title`}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" className={classes.durationCell}>
|
||||||
|
<span className={classes.formatBadge}>{EncodingName[track.encoding]}</span>
|
||||||
|
<span className={classes.durationCellTime}>{formatTimeFromFrames(track.duration, false)}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
return child;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Box className={classes.headBox}>
|
<Box className={classes.headBox}>
|
||||||
|
@ -372,40 +532,10 @@ export const Main = (props: {}) => {
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography component="h3" variant="h6" className={classes.toolbarLabel}>
|
<Typography component="h3" variant="h6" className={classes.toolbarLabel}>
|
||||||
|
{disc?.fullWidthTitle && `${disc.fullWidthTitle} / `}
|
||||||
{disc?.title || `Untitled Disc`}
|
{disc?.title || `Untitled Disc`}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{selectedCount === 1 ? (
|
|
||||||
<React.Fragment>
|
|
||||||
<Tooltip title="Move to Position">
|
|
||||||
<Button aria-controls="move-menu" aria-label="Move" onClick={handleShowMoveMenu}>
|
|
||||||
Move
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu
|
|
||||||
id="move-menu"
|
|
||||||
anchorEl={moveMenuAnchorEl}
|
|
||||||
open={!!moveMenuAnchorEl}
|
|
||||||
onClose={handleCloseMoveMenu}
|
|
||||||
PaperProps={{
|
|
||||||
style: {
|
|
||||||
maxHeight: 300,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array(tracks.length)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, i) => {
|
|
||||||
return (
|
|
||||||
<MenuItem key={`pos-${i}`} onClick={() => handleMoveSelectedTrack(i)}>
|
|
||||||
{i + 1}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
</React.Fragment>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{selectedCount > 0 ? (
|
{selectedCount > 0 ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Tooltip title="Record from MD">
|
<Tooltip title="Record from MD">
|
||||||
|
@ -424,6 +554,14 @@ export const Main = (props: {}) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{selectedCount > 0 ? (
|
||||||
|
<Tooltip title={canGroup ? 'Group' : ''}>
|
||||||
|
<IconButton aria-label="group" disabled={!canGroup} onClick={handleGroupTracks}>
|
||||||
|
<CreateNewFolderIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{selectedCount > 0 ? (
|
{selectedCount > 0 ? (
|
||||||
<Tooltip title="Rename">
|
<Tooltip title="Rename">
|
||||||
<IconButton aria-label="rename" disabled={selectedCount !== 1} onClick={handleRenameActionClick}>
|
<IconButton aria-label="rename" disabled={selectedCount !== 1} onClick={handleRenameActionClick}>
|
||||||
|
@ -432,7 +570,7 @@ export const Main = (props: {}) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<Box className={classes.main} {...getRootProps()}>
|
<Box className={classes.main} {...getRootProps()} id="main">
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
@ -442,64 +580,82 @@ export const Main = (props: {}) => {
|
||||||
<TableCell align="right">Duration</TableCell>
|
<TableCell align="right">Duration</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<DragDropContext onDragEnd={handleDrop}>
|
||||||
{tracks.map(track => (
|
<TableBody>
|
||||||
<TableRow
|
{groupedTracks.map((group, index) => (
|
||||||
hover
|
<TableRow key={`${index}`}>
|
||||||
selected={selected.includes(track.index)}
|
<TableCell colSpan={3} style={{ padding: '0' }}>
|
||||||
key={track.index}
|
<Table size="small">
|
||||||
onDoubleClick={event => handleRenameDoubleClick(event, track.index)}
|
<Droppable droppableId={`${index}`} key={`${index}`}>
|
||||||
onClick={event => handleSelectClick(event, track.index)}
|
{(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
|
||||||
className={classes.trackRow}
|
<TableBody
|
||||||
>
|
{...provided.droppableProps}
|
||||||
<TableCell className={classes.indexCell}>
|
ref={provided.innerRef}
|
||||||
{track.index === deviceStatus?.track && ['playing', 'paused'].includes(deviceStatus?.state) ? (
|
className={clsx({ [classes.hoveringOverGroup]: snapshot.isDraggingOver })}
|
||||||
<span>
|
>
|
||||||
<PlayArrowIcon
|
{group.title !== null && (
|
||||||
className={clsx(classes.controlButtonInTrackCommon, classes.playButtonInTrackListPlaying, {
|
<TableRow
|
||||||
[classes.currentControlButton]: deviceStatus?.state === 'playing',
|
hover
|
||||||
})}
|
className={classes.groupHeadRow}
|
||||||
onClick={event => {
|
key={`${index}-head`}
|
||||||
handleCurrentClick(event);
|
onDoubleClick={event =>
|
||||||
event.stopPropagation();
|
handleRenameDoubleClick(event, group.tracks[0].index, true)
|
||||||
}}
|
}
|
||||||
/>
|
>
|
||||||
<PauseIcon
|
<TableCell className={classes.indexCell}>
|
||||||
className={clsx(classes.controlButtonInTrackCommon, classes.pauseButtonInTrackListPlaying, {
|
<FolderIcon className={classes.controlButtonInTrackCommon} />
|
||||||
[classes.currentControlButton]: deviceStatus?.state === 'paused',
|
<DeleteIcon
|
||||||
})}
|
className={clsx(
|
||||||
onClick={event => {
|
classes.controlButtonInTrackCommon,
|
||||||
handleCurrentClick(event);
|
classes.deleteGroupButton
|
||||||
event.stopPropagation();
|
)}
|
||||||
}}
|
onClick={event => {
|
||||||
/>
|
handleGroupRemoval(event, group.tracks[0].index);
|
||||||
</span>
|
}}
|
||||||
) : (
|
/>
|
||||||
<span>
|
</TableCell>
|
||||||
<span className={classes.trackIndex}>{track.index + 1}</span>
|
<TableCell className={classes.titleCell} title={group.title!}>
|
||||||
<PlayArrowIcon
|
{group.fullWidthTitle ? `${group.fullWidthTitle} / ` : ``}
|
||||||
className={clsx(
|
{group.title || `No Name`}
|
||||||
classes.controlButtonInTrackCommon,
|
</TableCell>
|
||||||
classes.playButtonInTrackListNotPlaying
|
<TableCell align="right" className={classes.durationCell}>
|
||||||
|
<span className={classes.durationCellTime}>
|
||||||
|
{formatTimeFromFrames(
|
||||||
|
group.tracks.map(n => n.duration).reduce((a, b) => a + b),
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{group.title === null && group.tracks.length === 0 && (
|
||||||
|
<TableRow style={{ height: '1px' }} />
|
||||||
|
)}
|
||||||
|
{group.tracks.map(n => (
|
||||||
|
<Draggable
|
||||||
|
draggableId={`${group.index}-${n.index}`}
|
||||||
|
key={`${n.index - group.tracks[0].index}`}
|
||||||
|
index={n.index - group.tracks[0].index}
|
||||||
|
>
|
||||||
|
{(providedInGroup: DraggableProvided) =>
|
||||||
|
getTrackRow(n, group.title !== null, {
|
||||||
|
ref: providedInGroup.innerRef,
|
||||||
|
...providedInGroup.draggableProps,
|
||||||
|
...providedInGroup.dragHandleProps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</TableBody>
|
||||||
)}
|
)}
|
||||||
onClick={event => {
|
</Droppable>
|
||||||
handlePlayTrack(event, track.index);
|
</Table>
|
||||||
event.stopPropagation();
|
</TableCell>
|
||||||
}}
|
</TableRow>
|
||||||
/>
|
))}
|
||||||
</span>
|
</TableBody>
|
||||||
)}
|
</DragDropContext>
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.titleCell} title={track.title}>
|
|
||||||
{track.title || `No Title`}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right" className={classes.durationCell}>
|
|
||||||
<span className={classes.formatBadge}>{track.encoding}</span>
|
|
||||||
<span className={classes.durationCellTime}>{track.duration}</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
</Table>
|
||||||
<Backdrop className={classes.backdrop} open={isDragActive}>
|
<Backdrop className={classes.backdrop} open={isDragActive}>
|
||||||
Drop your Music to Upload
|
Drop your Music to Upload
|
||||||
|
|
|
@ -2,8 +2,9 @@ import React, { useCallback } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useShallowEqualSelector } from '../utils';
|
import { useShallowEqualSelector } from '../utils';
|
||||||
import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
|
import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
|
||||||
import { renameTrack, renameDisc } from '../redux/actions';
|
import { renameTrack, renameDisc, renameGroup } from '../redux/actions';
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Dialog from '@material-ui/core/Dialog';
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
import DialogActions from '@material-ui/core/DialogActions';
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
@ -21,25 +22,57 @@ const Transition = React.forwardRef(function Transition(
|
||||||
return <Slide direction="up" ref={ref} {...props} />;
|
return <Slide direction="up" ref={ref} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
marginUpDown: {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export const RenameDialog = (props: {}) => {
|
export const RenameDialog = (props: {}) => {
|
||||||
let dispatch = useDispatch();
|
let dispatch = useDispatch();
|
||||||
|
let classes = useStyles();
|
||||||
|
|
||||||
let renameDialogVisible = useShallowEqualSelector(state => state.renameDialog.visible);
|
let renameDialogVisible = useShallowEqualSelector(state => state.renameDialog.visible);
|
||||||
let renameDialogTitle = useShallowEqualSelector(state => state.renameDialog.title);
|
let renameDialogTitle = useShallowEqualSelector(state => state.renameDialog.title);
|
||||||
|
let renameDialogFullWidthTitle = useShallowEqualSelector(state => state.renameDialog.fullWidthTitle);
|
||||||
let renameDialogIndex = useShallowEqualSelector(state => state.renameDialog.index);
|
let renameDialogIndex = useShallowEqualSelector(state => state.renameDialog.index);
|
||||||
|
let renameDialogGroupIndex = useShallowEqualSelector(state => state.renameDialog.groupIndex);
|
||||||
|
let allowFullWidth = useShallowEqualSelector(state => state.appState.fullWidthSupport);
|
||||||
|
|
||||||
const what = renameDialogIndex < 0 ? `Disc` : `Track`;
|
const what = renameDialogGroupIndex !== null ? `Group` : renameDialogIndex < 0 ? `Disc` : `Track`;
|
||||||
|
|
||||||
const handleCancelRename = () => {
|
const handleCancelRename = () => {
|
||||||
dispatch(renameDialogActions.setVisible(false));
|
dispatch(renameDialogActions.setVisible(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDoRename = () => {
|
const handleDoRename = () => {
|
||||||
if (renameDialogIndex < 0) {
|
if (renameDialogGroupIndex !== null) {
|
||||||
dispatch(renameDisc({ newName: renameDialogTitle }));
|
// Just rename the group with this range
|
||||||
|
dispatch(
|
||||||
|
renameGroup({
|
||||||
|
newName: renameDialogTitle,
|
||||||
|
newFullWidthName: renameDialogFullWidthTitle,
|
||||||
|
groupIndex: renameDialogGroupIndex,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (renameDialogIndex < 0) {
|
||||||
|
dispatch(
|
||||||
|
renameDisc({
|
||||||
|
newName: renameDialogTitle,
|
||||||
|
newFullWidthName: renameDialogFullWidthTitle,
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(renameTrack({ index: renameDialogIndex, newName: renameDialogTitle }));
|
dispatch(
|
||||||
|
renameTrack({
|
||||||
|
index: renameDialogIndex,
|
||||||
|
newName: renameDialogTitle,
|
||||||
|
newFullWidthName: renameDialogFullWidthTitle,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
handleCancelRename(); // Close the dialog
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
|
@ -49,6 +82,13 @@ export const RenameDialog = (props: {}) => {
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleFullWidthChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
|
dispatch(renameDialogActions.setCurrentFullWidthName(event.target.value.substring(0, 105)));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const { vintageMode } = useShallowEqualSelector(state => state.appState);
|
const { vintageMode } = useShallowEqualSelector(state => state.appState);
|
||||||
if (vintageMode) {
|
if (vintageMode) {
|
||||||
const p = {
|
const p = {
|
||||||
|
@ -86,6 +126,20 @@ export const RenameDialog = (props: {}) => {
|
||||||
}}
|
}}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
{allowFullWidth && (
|
||||||
|
<TextField
|
||||||
|
id="fullWidthTitle"
|
||||||
|
label={`Full-Width ${what} Name`}
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
className={classes.marginUpDown}
|
||||||
|
value={renameDialogFullWidthTitle}
|
||||||
|
onKeyDown={event => {
|
||||||
|
event.key === `Enter` && handleDoRename();
|
||||||
|
}}
|
||||||
|
onChange={handleFullWidthChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCancelRename}>Cancel</Button>
|
<Button onClick={handleCancelRename}>Cancel</Button>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { batchActions } from 'redux-batched-actions';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import Menu from '@material-ui/core/Menu';
|
import Menu from '@material-ui/core/Menu';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import Divider from '@material-ui/core/Divider';
|
||||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||||
|
|
||||||
import { wipeDisc, listContent } from '../redux/actions';
|
import { wipeDisc, listContent } from '../redux/actions';
|
||||||
|
@ -14,6 +15,7 @@ import { useShallowEqualSelector } from '../utils';
|
||||||
import Link from '@material-ui/core/Link';
|
import Link from '@material-ui/core/Link';
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
@ -33,14 +35,19 @@ const useStyles = makeStyles(theme => ({
|
||||||
listItemIcon: {
|
listItemIcon: {
|
||||||
minWidth: theme.spacing(5),
|
minWidth: theme.spacing(5),
|
||||||
},
|
},
|
||||||
|
toolTippedText: {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
textDecorationStyle: 'dotted',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const TopMenu = function(props: { onClick?: () => void }) {
|
export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
let { mainView, darkMode, vintageMode } = useShallowEqualSelector(state => state.appState);
|
let { mainView, darkMode, vintageMode, fullWidthSupport } = useShallowEqualSelector(state => state.appState);
|
||||||
let discTitle = useShallowEqualSelector(state => state.main.disc?.title ?? ``);
|
let discTitle = useShallowEqualSelector(state => state.main.disc?.title ?? ``);
|
||||||
|
let fullWidthDiscTitle = useShallowEqualSelector(state => state.main.disc?.fullWidthTitle ?? ``);
|
||||||
|
|
||||||
const githubLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
const githubLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
||||||
const helpLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
const helpLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
||||||
|
@ -71,6 +78,10 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
}, [dispatch, handleMenuClose]);
|
}, [dispatch, handleMenuClose]);
|
||||||
|
|
||||||
|
const handleAllowFullWidth = useCallback(() => {
|
||||||
|
dispatch(appActions.setFullWidthSupport(!fullWidthSupport));
|
||||||
|
}, [dispatch, fullWidthSupport]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
dispatch(listContent());
|
dispatch(listContent());
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
|
@ -81,11 +92,13 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
batchActions([
|
batchActions([
|
||||||
renameDialogActions.setVisible(true),
|
renameDialogActions.setVisible(true),
|
||||||
renameDialogActions.setCurrentName(discTitle),
|
renameDialogActions.setCurrentName(discTitle),
|
||||||
|
renameDialogActions.setGroupIndex(null),
|
||||||
|
renameDialogActions.setCurrentFullWidthName(fullWidthDiscTitle),
|
||||||
renameDialogActions.setIndex(-1),
|
renameDialogActions.setIndex(-1),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
}, [dispatch, handleMenuClose, discTitle]);
|
}, [dispatch, handleMenuClose, discTitle, fullWidthDiscTitle]);
|
||||||
|
|
||||||
const handleExit = useCallback(() => {
|
const handleExit = useCallback(() => {
|
||||||
dispatch(appActions.setMainView('WELCOME'));
|
dispatch(appActions.setMainView('WELCOME'));
|
||||||
|
@ -156,6 +169,25 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (mainView === 'MAIN') {
|
||||||
|
menuItems.push(<Divider />);
|
||||||
|
menuItems.push(
|
||||||
|
<MenuItem key="allowFullWidth" onClick={handleAllowFullWidth}>
|
||||||
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
{fullWidthSupport ? <ToggleOnIcon fontSize="small" /> : <ToggleOffIcon fontSize="small" />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
{fullWidthSupport ? `Disable ` : `Enable `}
|
||||||
|
<Tooltip
|
||||||
|
title="This advanced feature enables the use of Hiragana and Kanji alphabets. More about this in Support and FAQ."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<span className={classes.toolTippedText}>Full-Width Title Editing</span>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<MenuItem key="darkMode" onClick={handleDarkMode}>
|
<MenuItem key="darkMode" onClick={handleDarkMode}>
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
@ -175,6 +207,9 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (mainView === 'MAIN') {
|
||||||
|
menuItems.push(<Divider />);
|
||||||
|
}
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<MenuItem key="about" onClick={handleShowAbout}>
|
<MenuItem key="about" onClick={handleShowAbout}>
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
|
|
@ -86,7 +86,8 @@ export const W95Main = (props: {
|
||||||
tracks: {
|
tracks: {
|
||||||
index: number;
|
index: number;
|
||||||
title: string;
|
title: string;
|
||||||
group: string;
|
fullWidthTitle: string;
|
||||||
|
group: string | null;
|
||||||
duration: string;
|
duration: string;
|
||||||
encoding: string;
|
encoding: string;
|
||||||
}[];
|
}[];
|
||||||
|
@ -122,6 +123,7 @@ export const W95Main = (props: {
|
||||||
<img alt="device" src={DeviceIconUrl} style={{ marginTop: -10, marginLeft: 10 }} />
|
<img alt="device" src={DeviceIconUrl} style={{ marginTop: -10, marginLeft: 10 }} />
|
||||||
<div className={classes.toolbarItem}>
|
<div className={classes.toolbarItem}>
|
||||||
{`${props.deviceName}: (` || `Loading...`}
|
{`${props.deviceName}: (` || `Loading...`}
|
||||||
|
{props.disc?.fullWidthTitle && `${props.disc?.fullWidthTitle} / `}
|
||||||
{props.disc?.title || `Untitled Disc`}
|
{props.disc?.title || `Untitled Disc`}
|
||||||
{`)`}
|
{`)`}
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,7 +204,7 @@ export const W95Main = (props: {
|
||||||
>
|
>
|
||||||
<TableDataCell style={{ textAlign: 'center', width: '2ch' }}>{track.index + 1}</TableDataCell>
|
<TableDataCell style={{ textAlign: 'center', width: '2ch' }}>{track.index + 1}</TableDataCell>
|
||||||
<TableDataCell style={{ width: '80%' }}>
|
<TableDataCell style={{ width: '80%' }}>
|
||||||
<div>{track.title || `No Title`}</div>
|
<div>{track.fullWidthTitle && `${track.fullWidthTitle} / `}{track.title || `No Title`}</div>
|
||||||
</TableDataCell>
|
</TableDataCell>
|
||||||
<TableDataCell style={{ textAlign: 'right', width: '20%' }}>
|
<TableDataCell style={{ textAlign: 'right', width: '20%' }}>
|
||||||
<span>{track.encoding}</span>
|
<span>{track.encoding}</span>
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const W95RenameDialog = (props: {
|
||||||
<span>Rename {props.what}</span>
|
<span>Rename {props.what}</span>
|
||||||
</WindowHeader>
|
</WindowHeader>
|
||||||
<WindowContent>
|
<WindowContent>
|
||||||
<p style={{ marginBottom: 4 }}>Track Name:</p>
|
<p style={{ marginBottom: 4 }}>{props.what} Name:</p>
|
||||||
<TextField
|
<TextField
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
value={props.renameDialogTitle}
|
value={props.renameDialogTitle}
|
||||||
|
|
|
@ -9,7 +9,15 @@ import { actions as mainActions } from './main-feature';
|
||||||
import serviceRegistry from '../services/registry';
|
import serviceRegistry from '../services/registry';
|
||||||
import { Wireformat, getTracks } from 'netmd-js';
|
import { Wireformat, getTracks } from 'netmd-js';
|
||||||
import { AnyAction } from '@reduxjs/toolkit';
|
import { AnyAction } from '@reduxjs/toolkit';
|
||||||
import { getAvailableCharsForTrackTitle, framesToSec, sleepWithProgressCallback, sleep, askNotificationPermission } from '../utils';
|
import {
|
||||||
|
getAvailableCharsForTitle,
|
||||||
|
framesToSec,
|
||||||
|
sleepWithProgressCallback,
|
||||||
|
sleep,
|
||||||
|
askNotificationPermission,
|
||||||
|
getGroupedTracks,
|
||||||
|
getHalfWidthTitleLength,
|
||||||
|
} from '../utils';
|
||||||
import * as mm from 'music-metadata-browser';
|
import * as mm from 'music-metadata-browser';
|
||||||
import { TitleFormatType, UploadFormat } from './convert-dialog-feature';
|
import { TitleFormatType, UploadFormat } from './convert-dialog-feature';
|
||||||
import NotificationCompleteIconUrl from '../images/record-complete-notification-icon.png';
|
import NotificationCompleteIconUrl from '../images/record-complete-notification-icon.png';
|
||||||
|
@ -50,6 +58,128 @@ export function control(action: 'play' | 'stop' | 'next' | 'prev' | 'goto' | 'pa
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renameGroup({ groupIndex, newName, newFullWidthName }: { groupIndex: number; newName: string; newFullWidthName?: string }) {
|
||||||
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
|
await serviceRegistry!.netmdService?.renameGroup(groupIndex, newName, newFullWidthName);
|
||||||
|
listContent()(dispatch);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupTracks(indexes: number[]) {
|
||||||
|
return async function(dispatch: AppDispatch) {
|
||||||
|
let begin = indexes[0];
|
||||||
|
let length = indexes[indexes.length - 1] - begin + 1;
|
||||||
|
const { netmdService } = serviceRegistry;
|
||||||
|
|
||||||
|
netmdService!.addGroup(begin, length, '');
|
||||||
|
listContent()(dispatch);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteGroup(groupBegin: number) {
|
||||||
|
return async function(dispatch: AppDispatch) {
|
||||||
|
const { netmdService } = serviceRegistry;
|
||||||
|
netmdService!.deleteGroup(groupBegin);
|
||||||
|
listContent()(dispatch);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dragDropTrack(sourceList: number, sourceIndex: number, targetList: number, targetIndex: number) {
|
||||||
|
// This code is here, because it would need to be duplicated in both netmd and netmd-mock.
|
||||||
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
|
if (sourceList === targetList && sourceIndex === targetIndex) return;
|
||||||
|
dispatch(appStateActions.setLoading(true));
|
||||||
|
const groupedTracks = getGroupedTracks(await serviceRegistry.netmdService!.listContent());
|
||||||
|
// Remove the moved item from its current list
|
||||||
|
let movedItem = groupedTracks[sourceList].tracks.splice(sourceIndex, 1)[0];
|
||||||
|
let newIndex: number;
|
||||||
|
|
||||||
|
// Calculate bounds
|
||||||
|
let boundsStartList, boundsEndList, boundsStartIndex, boundsEndIndex, offset;
|
||||||
|
|
||||||
|
if (sourceList < targetList) {
|
||||||
|
boundsStartList = sourceList;
|
||||||
|
boundsStartIndex = sourceIndex;
|
||||||
|
boundsEndList = targetList;
|
||||||
|
boundsEndIndex = targetIndex;
|
||||||
|
offset = -1;
|
||||||
|
} else if (sourceList > targetList) {
|
||||||
|
boundsStartList = targetList;
|
||||||
|
boundsStartIndex = targetIndex;
|
||||||
|
boundsEndList = sourceList;
|
||||||
|
boundsEndIndex = sourceIndex;
|
||||||
|
offset = 1;
|
||||||
|
} else {
|
||||||
|
if (sourceIndex < targetIndex) {
|
||||||
|
boundsStartList = boundsEndList = sourceList;
|
||||||
|
boundsStartIndex = sourceIndex;
|
||||||
|
boundsEndIndex = targetIndex;
|
||||||
|
offset = -1;
|
||||||
|
} else {
|
||||||
|
boundsStartList = boundsEndList = targetList;
|
||||||
|
boundsStartIndex = targetIndex;
|
||||||
|
boundsEndIndex = sourceIndex;
|
||||||
|
offset = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift indices
|
||||||
|
for (let i = boundsStartList; i <= boundsEndList; i++) {
|
||||||
|
let startingIndex = i === boundsStartList ? boundsStartIndex : 0;
|
||||||
|
let endingIndex = i === boundsEndList ? boundsEndIndex : groupedTracks[i].tracks.length;
|
||||||
|
for (let j = startingIndex; j < endingIndex; j++) {
|
||||||
|
groupedTracks[i].tracks[j].index += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the moved track's destination index
|
||||||
|
if (targetList === 0) {
|
||||||
|
newIndex = targetIndex;
|
||||||
|
} else {
|
||||||
|
if (targetIndex === 0) {
|
||||||
|
let prevList = groupedTracks[targetList - 1];
|
||||||
|
let i = 2;
|
||||||
|
while (prevList && prevList.tracks.length === 0) {
|
||||||
|
// Skip past all the empty lists
|
||||||
|
prevList = groupedTracks[targetList - i++];
|
||||||
|
}
|
||||||
|
if (prevList) {
|
||||||
|
// If there's a previous list, make this tracks's index previous list's last item's index + 1
|
||||||
|
let lastIndexOfPrevList = prevList.tracks[prevList.tracks.length - 1].index;
|
||||||
|
newIndex = lastIndexOfPrevList + 1;
|
||||||
|
} else newIndex = 0; // Else default to index 0
|
||||||
|
} else {
|
||||||
|
newIndex = groupedTracks[targetList].tracks[0].index + targetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movedItem.index !== newIndex) {
|
||||||
|
await serviceRegistry!.netmdService!.moveTrack(movedItem.index, newIndex, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
movedItem.index = newIndex;
|
||||||
|
groupedTracks[targetList].tracks.splice(targetIndex, 0, movedItem);
|
||||||
|
let ungrouped = [];
|
||||||
|
|
||||||
|
// Recompile the groups and update them on the player
|
||||||
|
let normalGroups = [];
|
||||||
|
for (let group of groupedTracks) {
|
||||||
|
if (group.tracks.length === 0) continue;
|
||||||
|
if (group.index === -1) ungrouped.push(...group.tracks);
|
||||||
|
else normalGroups.push(group);
|
||||||
|
}
|
||||||
|
if (ungrouped.length)
|
||||||
|
normalGroups.unshift({
|
||||||
|
index: 0,
|
||||||
|
title: null,
|
||||||
|
fullWidthTitle: null,
|
||||||
|
tracks: ungrouped,
|
||||||
|
});
|
||||||
|
await serviceRegistry.netmdService!.rewriteGroups(normalGroups);
|
||||||
|
listContent()(dispatch);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function pair() {
|
export function pair() {
|
||||||
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
dispatch(appStateActions.setPairingFailed(false));
|
dispatch(appStateActions.setPairingFailed(false));
|
||||||
|
@ -86,7 +216,15 @@ export function listContent() {
|
||||||
return async function(dispatch: AppDispatch) {
|
return async function(dispatch: AppDispatch) {
|
||||||
// Issue loading
|
// Issue loading
|
||||||
dispatch(appStateActions.setLoading(true));
|
dispatch(appStateActions.setLoading(true));
|
||||||
let disc = await serviceRegistry.netmdService!.listContent();
|
let disc;
|
||||||
|
try {
|
||||||
|
disc = await serviceRegistry.netmdService!.listContent();
|
||||||
|
} catch (err) {
|
||||||
|
if (window.confirm("This disc's title seems to be corrupted, do you wish to erase it?\nNone of the tracks will be deleted.")) {
|
||||||
|
await serviceRegistry.netmdService!.wipeDiscTitleInfo();
|
||||||
|
disc = await serviceRegistry.netmdService!.listContent();
|
||||||
|
} else throw err;
|
||||||
|
}
|
||||||
let deviceName = await serviceRegistry.netmdService!.getDeviceName();
|
let deviceName = await serviceRegistry.netmdService!.getDeviceName();
|
||||||
let deviceStatus = null;
|
let deviceStatus = null;
|
||||||
try {
|
try {
|
||||||
|
@ -105,12 +243,12 @@ export function listContent() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renameTrack({ index, newName }: { index: number; newName: string }) {
|
export function renameTrack({ index, newName, newFullWidthName }: { index: number; newName: string; newFullWidthName?: string }) {
|
||||||
return async function(dispatch: AppDispatch) {
|
return async function(dispatch: AppDispatch) {
|
||||||
const { netmdService } = serviceRegistry;
|
const { netmdService } = serviceRegistry;
|
||||||
dispatch(renameDialogActions.setVisible(false));
|
dispatch(renameDialogActions.setVisible(false));
|
||||||
try {
|
try {
|
||||||
await netmdService!.renameTrack(index, newName);
|
await netmdService!.renameTrack(index, newName, newFullWidthName);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
dispatch(batchActions([errorDialogAction.setVisible(true), errorDialogAction.setErrorMessage(`Rename failed.`)]));
|
dispatch(batchActions([errorDialogAction.setVisible(true), errorDialogAction.setErrorMessage(`Rename failed.`)]));
|
||||||
|
@ -119,10 +257,13 @@ export function renameTrack({ index, newName }: { index: number; newName: string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renameDisc({ newName }: { newName: string }) {
|
export function renameDisc({ newName, newFullWidthName }: { newName: string; newFullWidthName?: string }) {
|
||||||
return async function(dispatch: AppDispatch) {
|
return async function(dispatch: AppDispatch) {
|
||||||
const { netmdService } = serviceRegistry;
|
const { netmdService } = serviceRegistry;
|
||||||
await netmdService!.renameDisc(newName);
|
await netmdService!.renameDisc(
|
||||||
|
newName.replace(/\/\//g, ' /'), // Make sure the title doesn't interfere with the groups
|
||||||
|
newFullWidthName?.replace(////g, '/')
|
||||||
|
);
|
||||||
dispatch(renameDialogActions.setVisible(false));
|
dispatch(renameDialogActions.setVisible(false));
|
||||||
listContent()(dispatch);
|
listContent()(dispatch);
|
||||||
};
|
};
|
||||||
|
@ -138,11 +279,7 @@ export function deleteTracks(indexes: number[]) {
|
||||||
}
|
}
|
||||||
const { netmdService } = serviceRegistry;
|
const { netmdService } = serviceRegistry;
|
||||||
dispatch(appStateActions.setLoading(true));
|
dispatch(appStateActions.setLoading(true));
|
||||||
indexes = indexes.sort();
|
await netmdService!.deleteTracks(indexes);
|
||||||
indexes.reverse();
|
|
||||||
for (let index of indexes) {
|
|
||||||
await netmdService!.deleteTrack(index);
|
|
||||||
}
|
|
||||||
listContent()(dispatch);
|
listContent()(dispatch);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -387,8 +524,8 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma
|
||||||
};
|
};
|
||||||
|
|
||||||
let disc = getState().main.disc;
|
let disc = getState().main.disc;
|
||||||
let maxTitleLength = disc ? getAvailableCharsForTrackTitle(getTracks(disc).map(track => track.title || ``)) : -1;
|
let useFullWidth = getState().appState.fullWidthSupport;
|
||||||
maxTitleLength = Math.floor(maxTitleLength / files.length);
|
let availableCharacters = getAvailableCharsForTitle(disc!);
|
||||||
|
|
||||||
let error: any;
|
let error: any;
|
||||||
let errorMessage = ``;
|
let errorMessage = ``;
|
||||||
|
@ -407,16 +544,22 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTitleLength > -1) {
|
const fixLength = (l: number) => Math.ceil(l / 7) * 7;
|
||||||
title = title.substring(0, maxTitleLength);
|
let halfWidthTitle = title.substr(0, Math.min(getHalfWidthTitleLength(title), availableCharacters));
|
||||||
|
availableCharacters -= fixLength(getHalfWidthTitleLength(halfWidthTitle));
|
||||||
|
|
||||||
|
let fullWidthTitle = '';
|
||||||
|
if (useFullWidth) {
|
||||||
|
fullWidthTitle = title.substr(0, Math.min(title.length * 2, availableCharacters, 210 /* limit is 105 */) / 2);
|
||||||
|
availableCharacters -= fixLength(fullWidthTitle.length * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackUpdate.current = i++;
|
trackUpdate.current = i++;
|
||||||
trackUpdate.titleCurrent = title;
|
trackUpdate.titleCurrent = halfWidthTitle;
|
||||||
updateTrack();
|
updateTrack();
|
||||||
updateProgressCallback({ written: 0, encrypted: 0, total: 100 });
|
updateProgressCallback({ written: 0, encrypted: 0, total: 100 });
|
||||||
try {
|
try {
|
||||||
await netmdService?.upload(title, data, wireformat, updateProgressCallback);
|
await netmdService?.upload(halfWidthTitle, fullWidthTitle, data, wireformat, updateProgressCallback);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
errorMessage = `${file.name}: Error uploading to device. There might not be enough space left.`;
|
errorMessage = `${file.name}: Error uploading to device. There might not be enough space left.`;
|
||||||
|
|
|
@ -15,6 +15,7 @@ export interface AppState {
|
||||||
aboutDialogVisible: boolean;
|
aboutDialogVisible: boolean;
|
||||||
notifyWhenFinished: boolean;
|
notifyWhenFinished: boolean;
|
||||||
hasNotificationSupport: boolean;
|
hasNotificationSupport: boolean;
|
||||||
|
fullWidthSupport: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildInitialState = (): AppState => {
|
export const buildInitialState = (): AppState => {
|
||||||
|
@ -29,6 +30,7 @@ export const buildInitialState = (): AppState => {
|
||||||
aboutDialogVisible: false,
|
aboutDialogVisible: false,
|
||||||
notifyWhenFinished: loadPreference('notifyWhenFinished', false),
|
notifyWhenFinished: loadPreference('notifyWhenFinished', false),
|
||||||
hasNotificationSupport: true,
|
hasNotificationSupport: true,
|
||||||
|
fullWidthSupport: loadPreference('fullWidthSupport', false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,6 +74,10 @@ export const slice = createSlice({
|
||||||
showAboutDialog: (state, action: PayloadAction<boolean>) => {
|
showAboutDialog: (state, action: PayloadAction<boolean>) => {
|
||||||
state.aboutDialogVisible = action.payload;
|
state.aboutDialogVisible = action.payload;
|
||||||
},
|
},
|
||||||
|
setFullWidthSupport: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.fullWidthSupport = action.payload;
|
||||||
|
savePreference('fullWidthSupport', state.fullWidthSupport);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,17 @@ import { enableBatching } from 'redux-batched-actions';
|
||||||
export interface RenameDialogState {
|
export interface RenameDialogState {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
fullWidthTitle: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
groupIndex: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: RenameDialogState = {
|
const initialState: RenameDialogState = {
|
||||||
visible: false,
|
visible: false,
|
||||||
title: '',
|
title: '',
|
||||||
|
fullWidthTitle: '',
|
||||||
index: -1,
|
index: -1,
|
||||||
|
groupIndex: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const slice = createSlice({
|
export const slice = createSlice({
|
||||||
|
@ -23,9 +27,15 @@ export const slice = createSlice({
|
||||||
setCurrentName: (state: RenameDialogState, action: PayloadAction<string>) => {
|
setCurrentName: (state: RenameDialogState, action: PayloadAction<string>) => {
|
||||||
state.title = action.payload;
|
state.title = action.payload;
|
||||||
},
|
},
|
||||||
|
setCurrentFullWidthName: (state: RenameDialogState, action: PayloadAction<string>) => {
|
||||||
|
state.fullWidthTitle = action.payload;
|
||||||
|
},
|
||||||
setIndex: (state: RenameDialogState, action: PayloadAction<number>) => {
|
setIndex: (state: RenameDialogState, action: PayloadAction<number>) => {
|
||||||
state.index = action.payload;
|
state.index = action.payload;
|
||||||
},
|
},
|
||||||
|
setGroupIndex: (state: RenameDialogState, action: PayloadAction<number | null>) => {
|
||||||
|
state.groupIndex = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Track, Channels, Encoding, Wireformat, TrackFlag, DeviceStatus } from 'netmd-js';
|
import { Track, Channels, Encoding, Wireformat, TrackFlag, DeviceStatus, Group } from 'netmd-js';
|
||||||
import { NetMDService } from './netmd';
|
import { NetMDService } from './netmd';
|
||||||
import { sleep, sanitizeTitle, asyncMutex } from '../utils';
|
import { sleep, sanitizeFullWidthTitle, sanitizeHalfWidthTitle, asyncMutex, recomputeGroupsAfterTrackMove, isSequential } from '../utils';
|
||||||
import { assert } from 'netmd-js/dist/utils';
|
import { assert } from 'netmd-js/dist/utils';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ class NetMDMockService implements NetMDService {
|
||||||
public mutex = new Mutex();
|
public mutex = new Mutex();
|
||||||
public _tracksTitlesMaxLength = 1700;
|
public _tracksTitlesMaxLength = 1700;
|
||||||
public _discTitle: string = 'Mock Disc';
|
public _discTitle: string = 'Mock Disc';
|
||||||
|
public _fullWidthDiscTitle: string = '';
|
||||||
public _discCapacity: number = 80 * 60 * 512;
|
public _discCapacity: number = 80 * 60 * 512;
|
||||||
public _tracks: Track[] = [
|
public _tracks: Track[] = [
|
||||||
{
|
{
|
||||||
|
@ -18,6 +19,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Long name for - Mock Track 1 - by some artist -12398729837198723',
|
title: 'Long name for - Mock Track 1 - by some artist -12398729837198723',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
|
@ -26,6 +28,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Mock Track 2',
|
title: 'Mock Track 2',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
|
@ -34,6 +37,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Mock Track 3',
|
title: 'Mock Track 3',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
|
@ -42,8 +46,33 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Mock Track 4',
|
title: 'Mock Track 4',
|
||||||
|
fullWidthTitle: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 5 * 60 * 512,
|
||||||
|
encoding: Encoding.sp,
|
||||||
|
index: 4,
|
||||||
|
channel: Channels.stereo,
|
||||||
|
protected: TrackFlag.unprotected,
|
||||||
|
title: 'Mock Track 5',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
public _groups: Group[] = [
|
||||||
|
{
|
||||||
|
title: null,
|
||||||
|
index: 0,
|
||||||
|
tracks: this._tracks.slice(2),
|
||||||
|
fullWidthTitle: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Test',
|
||||||
|
fullWidthTitle: '',
|
||||||
|
index: 0,
|
||||||
|
tracks: [this._tracks[0], this._tracks[1]],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
public _status: DeviceStatus = {
|
public _status: DeviceStatus = {
|
||||||
discPresent: true,
|
discPresent: true,
|
||||||
track: 0,
|
track: 0,
|
||||||
|
@ -69,6 +98,20 @@ class NetMDMockService implements NetMDService {
|
||||||
return this._tracks.reduce((acc, track) => acc + (track.title?.length ?? 0), 0);
|
return this._tracks.reduce((acc, track) => acc + (track.title?.length ?? 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getDisc() {
|
||||||
|
return {
|
||||||
|
title: this._discTitle,
|
||||||
|
fullWidthTitle: this._fullWidthDiscTitle,
|
||||||
|
writeProtected: false,
|
||||||
|
writable: true,
|
||||||
|
left: this._discCapacity - this._getUsed(),
|
||||||
|
used: this._getUsed(),
|
||||||
|
total: this._discCapacity,
|
||||||
|
trackCount: this._tracks.length,
|
||||||
|
groups: this._groups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async pair() {
|
async pair() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -80,24 +123,58 @@ class NetMDMockService implements NetMDService {
|
||||||
async listContent() {
|
async listContent() {
|
||||||
// This object ends up in the state of redux and Immer will freeze it.
|
// This object ends up in the state of redux and Immer will freeze it.
|
||||||
// That's why it's deep cloned
|
// That's why it's deep cloned
|
||||||
return JSON.parse(
|
return JSON.parse(JSON.stringify(this._getDisc()));
|
||||||
JSON.stringify({
|
}
|
||||||
title: this._discTitle,
|
|
||||||
writeProtected: false,
|
async renameGroup(groupBegin: number, newName: string, newFullWidth?: string) {
|
||||||
writable: true,
|
let group = this._groups.slice(1).find(n => n.index === groupBegin);
|
||||||
left: this._discCapacity - this._getUsed(),
|
if (!group) return;
|
||||||
used: this._getUsed(),
|
group.title = newName;
|
||||||
total: this._discCapacity,
|
if (newFullWidth !== undefined) group.fullWidthTitle = newFullWidth;
|
||||||
trackCount: this._tracks.length,
|
}
|
||||||
groups: [
|
|
||||||
{
|
async addGroup(groupBegin: number, groupLength: number, newName: string) {
|
||||||
index: 0,
|
let ungrouped = this._groups.find(n => n.title === null);
|
||||||
title: null,
|
if (!ungrouped) return; // You can only group tracks that aren't already in a different group, if there's no such tracks, there's no point to continue
|
||||||
tracks: this._tracks,
|
let ungroupedLengthBeforeGroup = ungrouped.tracks.length;
|
||||||
},
|
|
||||||
],
|
let thisGroupTracks = ungrouped.tracks.filter(n => n.index >= groupBegin && n.index < groupBegin + groupLength);
|
||||||
})
|
ungrouped.tracks = ungrouped.tracks.filter(n => !thisGroupTracks.includes(n));
|
||||||
);
|
|
||||||
|
if (ungroupedLengthBeforeGroup - ungrouped.tracks.length !== groupLength) {
|
||||||
|
throw new Error('A track cannot be in 2 groups!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSequential(thisGroupTracks.map(n => n.index))) {
|
||||||
|
throw new Error('Invalid sequence of tracks!');
|
||||||
|
}
|
||||||
|
this._groups.push({
|
||||||
|
title: newName,
|
||||||
|
fullWidthTitle: '',
|
||||||
|
index: groupBegin,
|
||||||
|
tracks: thisGroupTracks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGroup(groupBegin: number) {
|
||||||
|
const thisGroup = this._groups.slice(1).find(n => n.tracks[0].index === groupBegin);
|
||||||
|
if (!thisGroup) return;
|
||||||
|
let ungroupedGroup = this._groups.find(n => n.title === null);
|
||||||
|
if (!ungroupedGroup) {
|
||||||
|
ungroupedGroup = {
|
||||||
|
title: null,
|
||||||
|
fullWidthTitle: null,
|
||||||
|
tracks: [],
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
this._groups.unshift(ungroupedGroup);
|
||||||
|
}
|
||||||
|
ungroupedGroup.tracks = ungroupedGroup.tracks.concat(thisGroup.tracks).sort((a, b) => a.index - b.index);
|
||||||
|
this._groups.splice(this._groups.indexOf(thisGroup), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rewriteGroups(groups: Group[]) {
|
||||||
|
this._groups = [...groups];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeviceStatus() {
|
async getDeviceStatus() {
|
||||||
|
@ -110,43 +187,69 @@ class NetMDMockService implements NetMDService {
|
||||||
|
|
||||||
async finalize() {}
|
async finalize() {}
|
||||||
|
|
||||||
async renameTrack(index: number, newTitle: string) {
|
async renameTrack(index: number, newTitle: string, fullWidthTitle?: string) {
|
||||||
newTitle = sanitizeTitle(newTitle);
|
newTitle = sanitizeHalfWidthTitle(newTitle);
|
||||||
if (this._getTracksTitlesLength() + newTitle.length > this._tracksTitlesMaxLength) {
|
if (this._getTracksTitlesLength() + newTitle.length > this._tracksTitlesMaxLength) {
|
||||||
throw new Error(`Track's title too long`);
|
throw new Error(`Track's title too long`);
|
||||||
}
|
}
|
||||||
|
if (fullWidthTitle !== undefined) {
|
||||||
|
this._tracks[index].fullWidthTitle = sanitizeFullWidthTitle(fullWidthTitle);
|
||||||
|
}
|
||||||
this._tracks[index].title = newTitle;
|
this._tracks[index].title = newTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
async renameDisc(newName: string) {
|
async renameDisc(newName: string, fullWidthName?: string) {
|
||||||
this._discTitle = newName;
|
this._discTitle = sanitizeHalfWidthTitle(newName);
|
||||||
|
if (fullWidthName !== undefined) this._fullWidthDiscTitle = sanitizeFullWidthTitle(fullWidthName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteTrack(index: number) {
|
async deleteTracks(indexes: number[]) {
|
||||||
this._tracks.splice(index, 1);
|
indexes = indexes.sort();
|
||||||
|
indexes.reverse();
|
||||||
|
for (let index of indexes) {
|
||||||
|
this._groups = recomputeGroupsAfterTrackMove(this._getDisc(), index, -1).groups;
|
||||||
|
this._tracks.splice(index, 1);
|
||||||
|
this._groups.forEach(n => (n.tracks = n.tracks.filter(n => this._tracks.includes(n))));
|
||||||
|
}
|
||||||
this._updateTrackIndexes();
|
this._updateTrackIndexes();
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveTrack(src: number, dst: number) {
|
async moveTrack(src: number, dst: number, updateGroups?: boolean) {
|
||||||
let t = this._tracks.splice(src, 1);
|
let t = this._tracks.splice(src, 1);
|
||||||
assert(t.length === 1);
|
assert(t.length === 1);
|
||||||
this._tracks.splice(dst, 0, t[0]);
|
this._tracks.splice(dst, 0, t[0]);
|
||||||
this._updateTrackIndexes();
|
this._updateTrackIndexes();
|
||||||
|
if (updateGroups || updateGroups === undefined) this._groups = recomputeGroupsAfterTrackMove(this._getDisc(), src, dst).groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
async wipeDisc() {
|
async wipeDisc() {
|
||||||
this._tracks = [];
|
this._tracks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async wipeDiscTitleInfo() {
|
||||||
|
this._groups = [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
title: null,
|
||||||
|
fullWidthTitle: null,
|
||||||
|
tracks: this._tracks,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this._discTitle = '';
|
||||||
|
this._fullWidthDiscTitle = '';
|
||||||
|
}
|
||||||
|
|
||||||
async upload(
|
async upload(
|
||||||
title: string,
|
title: string,
|
||||||
|
fullWidthTitle: string,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer,
|
||||||
format: Wireformat,
|
format: Wireformat,
|
||||||
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
||||||
) {
|
) {
|
||||||
progressCallback({ written: 0, encrypted: 0, total: 100 });
|
progressCallback({ written: 0, encrypted: 0, total: 100 });
|
||||||
|
|
||||||
title = sanitizeTitle(title);
|
let halfWidthTitle = sanitizeHalfWidthTitle(title);
|
||||||
|
fullWidthTitle = sanitizeFullWidthTitle(fullWidthTitle);
|
||||||
|
|
||||||
if (this._getTracksTitlesLength() + title.length > this._tracksTitlesMaxLength) {
|
if (this._getTracksTitlesLength() + title.length > this._tracksTitlesMaxLength) {
|
||||||
throw new Error(`Track's title too long`); // Simulates reject from device
|
throw new Error(`Track's title too long`); // Simulates reject from device
|
||||||
|
@ -160,12 +263,13 @@ class NetMDMockService implements NetMDService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tracks.push({
|
this._tracks.push({
|
||||||
title,
|
title: halfWidthTitle,
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
encoding: Encoding.sp,
|
encoding: Encoding.sp,
|
||||||
index: this._tracks.length,
|
index: this._tracks.length,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
channel: 0,
|
channel: 0,
|
||||||
|
fullWidthTitle: fullWidthTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
|
@ -9,10 +9,19 @@ import {
|
||||||
download,
|
download,
|
||||||
getDeviceStatus,
|
getDeviceStatus,
|
||||||
DeviceStatus,
|
DeviceStatus,
|
||||||
|
Group,
|
||||||
} from 'netmd-js';
|
} from 'netmd-js';
|
||||||
import { makeGetAsyncPacketIteratorOnWorkerThread } from 'netmd-js/dist/web-encrypt-worker';
|
import { makeGetAsyncPacketIteratorOnWorkerThread } from 'netmd-js/dist/web-encrypt-worker';
|
||||||
import { Logger } from 'netmd-js/dist/logger';
|
import { Logger } from 'netmd-js/dist/logger';
|
||||||
import { asyncMutex, sanitizeTitle, sleep } from '../utils';
|
import {
|
||||||
|
asyncMutex,
|
||||||
|
sanitizeHalfWidthTitle,
|
||||||
|
sanitizeFullWidthTitle,
|
||||||
|
sleep,
|
||||||
|
isSequential,
|
||||||
|
compileDiscTitles,
|
||||||
|
recomputeGroupsAfterTrackMove,
|
||||||
|
} from '../utils';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
const Worker = require('worker-loader!netmd-js/dist/web-encrypt-worker.js'); // eslint-disable-line import/no-webpack-loader-syntax
|
const Worker = require('worker-loader!netmd-js/dist/web-encrypt-worker.js'); // eslint-disable-line import/no-webpack-loader-syntax
|
||||||
|
@ -25,13 +34,19 @@ export interface NetMDService {
|
||||||
listContent(): Promise<Disc>;
|
listContent(): Promise<Disc>;
|
||||||
getDeviceName(): Promise<string>;
|
getDeviceName(): Promise<string>;
|
||||||
finalize(): Promise<void>;
|
finalize(): Promise<void>;
|
||||||
renameTrack(index: number, newTitle: string): Promise<void>;
|
renameTrack(index: number, newTitle: string, newFullWidthTitle?: string): Promise<void>;
|
||||||
renameDisc(newName: string): Promise<void>;
|
renameDisc(newName: string, newFullWidthName?: string): Promise<void>;
|
||||||
deleteTrack(index: number): Promise<void>;
|
renameGroup(groupBegin: number, newTitle: string, newFullWidthTitle?: string): Promise<void>;
|
||||||
moveTrack(src: number, dst: number): Promise<void>;
|
addGroup(groupBegin: number, groupLength: number, name: string): Promise<void>;
|
||||||
|
deleteGroup(groupIndex: number): Promise<void>;
|
||||||
|
rewriteGroups(groups: Group[]): Promise<void>;
|
||||||
|
deleteTracks(indexes: number[]): Promise<void>;
|
||||||
|
moveTrack(src: number, dst: number, updateGroups?: boolean): Promise<void>;
|
||||||
wipeDisc(): Promise<void>;
|
wipeDisc(): Promise<void>;
|
||||||
|
wipeDiscTitleInfo(): Promise<void>;
|
||||||
upload(
|
upload(
|
||||||
title: string,
|
title: string,
|
||||||
|
fullWidthTitle: string,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer,
|
||||||
format: Wireformat,
|
format: Wireformat,
|
||||||
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
||||||
|
@ -49,6 +64,7 @@ export interface NetMDService {
|
||||||
export class NetMDUSBService implements NetMDService {
|
export class NetMDUSBService implements NetMDService {
|
||||||
private netmdInterface?: NetMDInterface;
|
private netmdInterface?: NetMDInterface;
|
||||||
private logger?: Logger;
|
private logger?: Logger;
|
||||||
|
private cachedContentList?: Disc;
|
||||||
public mutex = new Mutex();
|
public mutex = new Mutex();
|
||||||
public statusMonitorTimer: any;
|
public statusMonitorTimer: any;
|
||||||
|
|
||||||
|
@ -70,7 +86,33 @@ export class NetMDUSBService implements NetMDService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async writeRawTitles(titleObject: { newRawTitle: string; newRawFullWidthTitle: string } | null) {
|
||||||
|
if (titleObject === null) return;
|
||||||
|
await this.netmdInterface!.cacheTOC();
|
||||||
|
await this.netmdInterface!.setDiscTitle(sanitizeHalfWidthTitle(titleObject.newRawTitle));
|
||||||
|
await this.netmdInterface!.setDiscTitle(sanitizeFullWidthTitle(titleObject.newRawFullWidthTitle), true);
|
||||||
|
await this.netmdInterface!.syncTOC();
|
||||||
|
this.dropCachedContentList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listContentUsingCache() {
|
||||||
|
// listContent takes a long time to execute (>3000ms), so I think caching it should speed up the app
|
||||||
|
if (!this.cachedContentList) {
|
||||||
|
console.log("There's no cached version of the TOC, caching");
|
||||||
|
this.cachedContentList = await listContent(this.netmdInterface!);
|
||||||
|
} else {
|
||||||
|
console.log("There's a cached TOC available.");
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(this.cachedContentList)) as Disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private dropCachedContentList() {
|
||||||
|
console.log('Cached TOC Dropped');
|
||||||
|
this.cachedContentList = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async pair() {
|
async pair() {
|
||||||
|
this.dropCachedContentList();
|
||||||
let iface = await openNewDevice(navigator.usb, this.logger);
|
let iface = await openNewDevice(navigator.usb, this.logger);
|
||||||
if (iface === null) {
|
if (iface === null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -80,6 +122,7 @@ export class NetMDUSBService implements NetMDService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
|
this.dropCachedContentList();
|
||||||
let iface = await openPairedDevice(navigator.usb, this.logger);
|
let iface = await openPairedDevice(navigator.usb, this.logger);
|
||||||
if (iface === null) {
|
if (iface === null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -90,7 +133,8 @@ export class NetMDUSBService implements NetMDService {
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async listContent() {
|
async listContent() {
|
||||||
return await listContent(this.netmdInterface!);
|
this.dropCachedContentList();
|
||||||
|
return await this.listContentUsingCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
|
@ -106,24 +150,109 @@ export class NetMDUSBService implements NetMDService {
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async finalize() {
|
async finalize() {
|
||||||
await this.netmdInterface!.netMd.finalize();
|
await this.netmdInterface!.netMd.finalize();
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async renameTrack(index: number, title: string) {
|
async rewriteGroups(groups: Group[]) {
|
||||||
// Removing non ascii chars... Sorry, I didn't implement char encoding.
|
const disc = await this.listContentUsingCache();
|
||||||
title = sanitizeTitle(title);
|
disc.groups = groups;
|
||||||
|
await this.writeRawTitles(compileDiscTitles(disc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@asyncMutex
|
||||||
|
async renameTrack(index: number, title: string, fullWidthTitle?: string) {
|
||||||
|
title = sanitizeHalfWidthTitle(title);
|
||||||
await this.netmdInterface!.cacheTOC();
|
await this.netmdInterface!.cacheTOC();
|
||||||
await this.netmdInterface!.setTrackTitle(index, title);
|
await this.netmdInterface!.setTrackTitle(index, title);
|
||||||
|
if (fullWidthTitle !== undefined) {
|
||||||
|
await this.netmdInterface!.setTrackTitle(index, sanitizeFullWidthTitle(fullWidthTitle), true);
|
||||||
|
}
|
||||||
await this.netmdInterface!.syncTOC();
|
await this.netmdInterface!.syncTOC();
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async renameDisc(newName: string) {
|
async renameGroup(groupBegin: number, newName: string, newFullWidthName?: string) {
|
||||||
|
const disc = await this.listContentUsingCache();
|
||||||
|
let thisGroup = disc.groups.find(n => n.tracks[0].index === groupBegin);
|
||||||
|
if (!thisGroup) return;
|
||||||
|
|
||||||
|
thisGroup.title = newName;
|
||||||
|
if (newFullWidthName !== undefined) thisGroup.fullWidthTitle = newFullWidthName;
|
||||||
|
await this.writeRawTitles(compileDiscTitles(disc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@asyncMutex
|
||||||
|
async addGroup(groupBegin: number, groupLength: number, title: string) {
|
||||||
|
const disc = await this.listContentUsingCache();
|
||||||
|
let ungrouped = disc.groups.find(n => n.title === null);
|
||||||
|
if (!ungrouped) return; // You can only group tracks that aren't already in a different group, if there's no such tracks, there's no point to continue
|
||||||
|
let ungroupedLengthBeforeGroup = ungrouped.tracks.length;
|
||||||
|
|
||||||
|
let thisGroupTracks = ungrouped.tracks.filter(n => n.index >= groupBegin && n.index < groupBegin + groupLength);
|
||||||
|
ungrouped.tracks = ungrouped.tracks.filter(n => !thisGroupTracks.includes(n));
|
||||||
|
|
||||||
|
if (ungroupedLengthBeforeGroup - ungrouped.tracks.length !== groupLength) {
|
||||||
|
throw new Error('A track cannot be in 2 groups!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSequential(thisGroupTracks.map(n => n.index))) {
|
||||||
|
throw new Error('Invalid sequence of tracks!');
|
||||||
|
}
|
||||||
|
disc.groups.push({
|
||||||
|
title,
|
||||||
|
fullWidthTitle: '',
|
||||||
|
index: groupBegin,
|
||||||
|
tracks: thisGroupTracks,
|
||||||
|
});
|
||||||
|
await this.writeRawTitles(compileDiscTitles(disc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@asyncMutex
|
||||||
|
async deleteGroup(groupBegin: number) {
|
||||||
|
const disc = await this.listContentUsingCache();
|
||||||
|
|
||||||
|
let thisGroup = disc.groups.find(n => n.tracks[0].index === groupBegin);
|
||||||
|
if (thisGroup) disc.groups.splice(disc.groups.indexOf(thisGroup), 1);
|
||||||
|
|
||||||
|
await this.writeRawTitles(compileDiscTitles(disc));
|
||||||
|
}
|
||||||
|
|
||||||
|
@asyncMutex
|
||||||
|
async renameDisc(newName: string, newFullWidthName?: string) {
|
||||||
// TODO: This whole function should be moved in netmd-js
|
// TODO: This whole function should be moved in netmd-js
|
||||||
const oldName = await this.netmdInterface!.getDiscTitle();
|
const oldName = await this.netmdInterface!.getDiscTitle();
|
||||||
|
const oldFullWidthName = await this.netmdInterface!.getDiscTitle(true);
|
||||||
const oldRawName = await this.netmdInterface!._getDiscTitle();
|
const oldRawName = await this.netmdInterface!._getDiscTitle();
|
||||||
|
const oldRawFullWidthName = await this.netmdInterface!._getDiscTitle(true);
|
||||||
const hasGroups = oldRawName.indexOf('//') >= 0;
|
const hasGroups = oldRawName.indexOf('//') >= 0;
|
||||||
|
const hasFullWidthGroups = oldRawName.indexOf('//') >= 0;
|
||||||
const hasGroupsAndTitle = oldRawName.startsWith('0;');
|
const hasGroupsAndTitle = oldRawName.startsWith('0;');
|
||||||
|
const hasFullWidthGroupsAndTitle = oldRawName.startsWith('0;');
|
||||||
|
|
||||||
|
newName = sanitizeHalfWidthTitle(newName);
|
||||||
|
newFullWidthName = newFullWidthName && sanitizeFullWidthTitle(newFullWidthName);
|
||||||
|
|
||||||
|
if (newFullWidthName !== oldFullWidthName && newFullWidthName !== undefined) {
|
||||||
|
let newFullWidthNameWithGroups;
|
||||||
|
if (hasFullWidthGroups) {
|
||||||
|
if (hasFullWidthGroupsAndTitle) {
|
||||||
|
newFullWidthNameWithGroups = oldRawFullWidthName.replace(
|
||||||
|
/^0;.*?///,
|
||||||
|
newFullWidthName !== '' ? `0;${newFullWidthName}//` : ``
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newFullWidthNameWithGroups = `0;${newFullWidthName}//${oldRawFullWidthName}`; // Add the new title
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newFullWidthNameWithGroups = newFullWidthName;
|
||||||
|
}
|
||||||
|
await this.netmdInterface!.cacheTOC();
|
||||||
|
await this.netmdInterface!.setDiscTitle(newFullWidthNameWithGroups, true);
|
||||||
|
await this.netmdInterface!.syncTOC();
|
||||||
|
this.dropCachedContentList();
|
||||||
|
}
|
||||||
|
|
||||||
if (newName === oldName) {
|
if (newName === oldName) {
|
||||||
return;
|
return;
|
||||||
|
@ -144,26 +273,50 @@ export class NetMDUSBService implements NetMDService {
|
||||||
await this.netmdInterface!.cacheTOC();
|
await this.netmdInterface!.cacheTOC();
|
||||||
await this.netmdInterface!.setDiscTitle(newNameWithGroups);
|
await this.netmdInterface!.setDiscTitle(newNameWithGroups);
|
||||||
await this.netmdInterface!.syncTOC();
|
await this.netmdInterface!.syncTOC();
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async deleteTrack(index: number) {
|
async deleteTracks(indexes: number[]) {
|
||||||
await this.netmdInterface!.eraseTrack(index);
|
indexes = indexes.sort();
|
||||||
await sleep(100);
|
indexes.reverse();
|
||||||
|
let content = await this.listContentUsingCache();
|
||||||
|
for (let index of indexes) {
|
||||||
|
content = recomputeGroupsAfterTrackMove(content, index, -1);
|
||||||
|
await this.netmdInterface!.eraseTrack(index);
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
await this.writeRawTitles(compileDiscTitles(content));
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async wipeDisc() {
|
async wipeDisc() {
|
||||||
await this.netmdInterface!.eraseDisc();
|
await this.netmdInterface!.eraseDisc();
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async moveTrack(src: number, dst: number) {
|
async wipeDiscTitleInfo() {
|
||||||
|
await this.writeRawTitles({
|
||||||
|
newRawTitle: '',
|
||||||
|
newRawFullWidthTitle: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@asyncMutex
|
||||||
|
async moveTrack(src: number, dst: number, updateGroups?: boolean) {
|
||||||
await this.netmdInterface!.moveTrack(src, dst);
|
await this.netmdInterface!.moveTrack(src, dst);
|
||||||
|
|
||||||
|
if (updateGroups === undefined || updateGroups) {
|
||||||
|
await this.writeRawTitles(compileDiscTitles(recomputeGroupsAfterTrackMove(await this.listContentUsingCache(), src, dst)));
|
||||||
|
}
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload(
|
async upload(
|
||||||
title: string,
|
title: string,
|
||||||
|
fullWidthTitle: string,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer,
|
||||||
format: Wireformat,
|
format: Wireformat,
|
||||||
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
||||||
|
@ -182,9 +335,9 @@ export class NetMDUSBService implements NetMDService {
|
||||||
updateProgress();
|
updateProgress();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Removing non ascii chars... Sorry, I didn't implement char encoding.
|
let halfWidthTitle = sanitizeHalfWidthTitle(title);
|
||||||
title = sanitizeTitle(title);
|
fullWidthTitle = sanitizeFullWidthTitle(fullWidthTitle);
|
||||||
let mdTrack = new MDTrack(title, format, data, 0x80000, webWorkerAsyncPacketIterator);
|
let mdTrack = new MDTrack(halfWidthTitle, format, data, 0x80000, fullWidthTitle, webWorkerAsyncPacketIterator);
|
||||||
|
|
||||||
await download(this.netmdInterface!, mdTrack, ({ writtenBytes }) => {
|
await download(this.netmdInterface!, mdTrack, ({ writtenBytes }) => {
|
||||||
written = writtenBytes;
|
written = writtenBytes;
|
||||||
|
@ -192,6 +345,7 @@ export class NetMDUSBService implements NetMDService {
|
||||||
});
|
});
|
||||||
|
|
||||||
w.terminate();
|
w.terminate();
|
||||||
|
this.dropCachedContentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
|
|
|
@ -9,3 +9,4 @@ declare module 'react95';
|
||||||
declare module 'react95/dist/themes/original';
|
declare module 'react95/dist/themes/original';
|
||||||
declare module 'react95/dist/fonts/ms_sans_serif.woff2';
|
declare module 'react95/dist/fonts/ms_sans_serif.woff2';
|
||||||
declare module 'react95/dist/fonts/ms_sans_serif_bold.woff2';
|
declare module 'react95/dist/fonts/ms_sans_serif_bold.woff2';
|
||||||
|
declare module 'jconv';
|
||||||
|
|
272
src/utils.ts
272
src/utils.ts
|
@ -1,8 +1,10 @@
|
||||||
import { Disc, formatTimeFromFrames, Encoding } from 'netmd-js';
|
import { Disc, formatTimeFromFrames, Encoding, getTracks, Group } from 'netmd-js';
|
||||||
import { useSelector, shallowEqual } from 'react-redux';
|
import { useSelector, shallowEqual } from 'react-redux';
|
||||||
import { RootState } from './redux/store';
|
import { RootState } from './redux/store';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
import { Theme } from '@material-ui/core';
|
import { Theme } from '@material-ui/core';
|
||||||
|
import jconv from 'jconv';
|
||||||
|
import { halfWidthToFullWidthRange } from 'netmd-js/dist/utils';
|
||||||
|
|
||||||
export function sleep(ms: number) {
|
export function sleep(ms: number) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -49,12 +51,33 @@ export function loadPreference<T>(key: string, defaultValue: T): T {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailableCharsForTrackTitle(trackTitles: string[]) {
|
export function getAvailableCharsForTitle(disc: Disc, includeGroups?: boolean) {
|
||||||
const maxChars = 1700; // see https://www.minidisc.org/md_toc.html
|
const cellLimit = 255;
|
||||||
const usedChars = trackTitles.reduce((acc, title) => {
|
// see https://www.minidisc.org/md_toc.html
|
||||||
return acc + title.length;
|
const fixLength = (len: number) => Math.ceil(len / 7);
|
||||||
}, 0);
|
|
||||||
return maxChars - usedChars;
|
let groups = disc.groups.filter(n => n.title !== null);
|
||||||
|
|
||||||
|
// Assume worst-case scenario
|
||||||
|
let fwTitle = disc.fullWidthTitle + `0;//`;
|
||||||
|
let hwTitle = disc.title + `0;//`;
|
||||||
|
if (includeGroups || includeGroups === undefined)
|
||||||
|
for (let group of groups) {
|
||||||
|
let range = `${group.tracks[0].index + 1}${group.tracks.length - 1 !== 0 &&
|
||||||
|
`-${group.tracks[group.tracks.length - 1].index + 1}`}//`;
|
||||||
|
// The order of these characters doesn't matter. It's for length only
|
||||||
|
fwTitle += group.fullWidthTitle + range;
|
||||||
|
hwTitle += group.title + range;
|
||||||
|
}
|
||||||
|
|
||||||
|
let usedCells = 0;
|
||||||
|
usedCells += fixLength(fwTitle.length * 2);
|
||||||
|
usedCells += fixLength(getHalfWidthTitleLength(hwTitle));
|
||||||
|
for (let trk of getTracks(disc)) {
|
||||||
|
usedCells += fixLength((trk.fullWidthTitle?.length ?? 0) * 2);
|
||||||
|
usedCells += fixLength(getHalfWidthTitleLength(trk.title ?? ''));
|
||||||
|
}
|
||||||
|
return Math.max(cellLimit - usedCells, 0) * 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function framesToSec(frames: number) {
|
export function framesToSec(frames: number) {
|
||||||
|
@ -65,21 +88,78 @@ export function sanitizeTitle(title: string) {
|
||||||
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
|
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const EncodingName: { [k: number]: string } = {
|
export function getHalfWidthTitleLength(title: string) {
|
||||||
|
// Some characters are written as 2 bytes
|
||||||
|
// prettier-ignore
|
||||||
|
const multiByteChars: { [key: string]: number } = { "ガ": 1, "ギ": 1, "グ": 1, "ゲ": 1, "ゴ": 1, "ザ": 1, "ジ": 1, "ズ": 1, "ゼ": 1, "ゾ": 1, "ダ": 1, "ヂ": 1, "ヅ": 1, "デ": 1, "ド": 1, "バ": 1, "パ": 1, "ビ": 1, "ピ": 1, "ブ": 1, "プ": 1, "ベ": 1, "ペ": 1, "ボ": 1, "ポ": 1, "ヮ": 1, "ヰ": 1, "ヱ": 1, "ヵ": 1, "ヶ": 1, "ヴ": 1, "ヽ": 1, "ヾ": 1, "が": 1, "ぎ": 1, "ぐ": 1, "げ": 1, "ご": 1, "ざ": 1, "じ": 1, "ず": 1, "ぜ": 1, "ぞ": 1, "だ": 1, "ぢ": 1, "づ": 1, "で": 1, "ど": 1, "ば": 1, "ぱ": 1, "び": 1, "ぴ": 1, "ぶ": 1, "ぷ": 1, "べ": 1, "ぺ": 1, "ぼ": 1, "ぽ": 1, "ゎ": 1, "ゐ": 1, "ゑ": 1, "ゕ": 1, "ゖ": 1, "ゔ": 1, "ゝ": 1, "ゞ": 1 };
|
||||||
|
return (
|
||||||
|
title.length +
|
||||||
|
title
|
||||||
|
.split('')
|
||||||
|
.map(n => multiByteChars[n] ?? 0)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeHalfWidthTitle(title: string) {
|
||||||
|
// prettier-ignore
|
||||||
|
const mappings: { [key: string]: string } = { 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、', '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', ''': "'", '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\': '\\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', '\u3000': ' ', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ぁ': 'ァ', 'あ': 'ア', 'ぃ': 'ィ', 'い': 'イ', 'ぅ': 'ゥ', 'う': 'ウ', 'ぇ': 'ェ', 'え': 'エ', 'ぉ': 'ォ', 'お': 'オ', 'か': 'カ', 'が': 'ガ', 'き': 'キ', 'ぎ': 'ギ', 'く': 'ク', 'ぐ': 'グ', 'け': 'ケ', 'げ': 'ゲ', 'こ': 'コ', 'ご': 'ゴ', 'さ': 'サ', 'ざ': 'ザ', 'し': 'シ', 'じ': 'ジ', 'す': 'ス', 'ず': 'ズ', 'せ': 'セ', 'ぜ': 'ゼ', 'そ': 'ソ', 'ぞ': 'ゾ', 'た': 'タ', 'だ': 'ダ', 'ち': 'チ', 'ぢ': 'ヂ', 'っ': 'ッ', 'つ': 'ツ', 'づ': 'ヅ', 'て': 'テ', 'で': 'デ', 'と': 'ト', 'ど': 'ド', 'な': 'ナ', 'に': 'ニ', 'ぬ': 'ヌ', 'ね': 'ネ', 'の': 'ノ', 'は': 'ハ', 'ば': 'バ', 'ぱ': 'パ', 'ひ': 'ヒ', 'び': 'ビ', 'ぴ': 'ピ', 'ふ': 'フ', 'ぶ': 'ブ', 'ぷ': 'プ', 'へ': 'ヘ', 'べ': 'ベ', 'ぺ': 'ペ', 'ほ': 'ホ', 'ぼ': 'ボ', 'ぽ': 'ポ', 'ま': 'マ', 'み': 'ミ', 'む': 'ム', 'め': 'メ', 'も': 'モ', 'ゃ': 'ャ', 'や': 'ヤ', 'ゅ': 'ュ', 'ゆ': 'ユ', 'ょ': 'ョ', 'よ': 'ヨ', 'ら': 'ラ', 'り': 'リ', 'る': 'ル', 'れ': 'レ', 'ろ': 'ロ', 'わ': 'ワ', 'を': 'ヲ', 'ん': 'ン', 'ゎ': 'ヮ', 'ゐ': 'ヰ', 'ゑ': 'ヱ', 'ゕ': 'ヵ', 'ゖ': 'ヶ', 'ゔ': 'ヴ', 'ゝ': 'ヽ', 'ゞ': 'ヾ' };
|
||||||
|
const allowedHalfWidthKana: string[] = Object.values(mappings);
|
||||||
|
|
||||||
|
const newTitle = title
|
||||||
|
.split('')
|
||||||
|
.map(n => {
|
||||||
|
if (mappings[n]) return mappings[n];
|
||||||
|
if (n.charCodeAt(0) < 0x7f || allowedHalfWidthKana.includes(n)) return n;
|
||||||
|
return ' ';
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
// Check if the amount of characters is the same as the amount of encoded bytes (when accounting for dakuten). Otherwise the disc might end up corrupted
|
||||||
|
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
||||||
|
if (sjisEncoded.length !== getHalfWidthTitleLength(title)) return sanitizeTitle(title); //Fallback
|
||||||
|
return newTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeFullWidthTitle(title: string) {
|
||||||
|
// prettier-ignore
|
||||||
|
const mappings: { [key: string]: string } = { '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', "'": ''', '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\\': '\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', ' ': '\u3000', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、' };
|
||||||
|
|
||||||
|
const newTitle = title
|
||||||
|
.split('')
|
||||||
|
.map(n => mappings[n] ?? n)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
||||||
|
if (jconv.decode(sjisEncoded, 'SJIS') !== newTitle) return sanitizeTitle(title); // Fallback
|
||||||
|
if (sjisEncoded.length !== title.length * 2) return sanitizeTitle(title); // Fallback (every character in the full-width title is 2 bytes)
|
||||||
|
return newTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EncodingName: { [k: number]: string } = {
|
||||||
[Encoding.sp]: 'SP',
|
[Encoding.sp]: 'SP',
|
||||||
[Encoding.lp2]: 'LP2',
|
[Encoding.lp2]: 'LP2',
|
||||||
[Encoding.lp4]: 'LP4',
|
[Encoding.lp4]: 'LP4',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DisplayTrack = {
|
||||||
|
index: number;
|
||||||
|
title: string;
|
||||||
|
fullWidthTitle: string;
|
||||||
|
group: string | null;
|
||||||
|
duration: string;
|
||||||
|
encoding: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function getSortedTracks(disc: Disc | null) {
|
export function getSortedTracks(disc: Disc | null) {
|
||||||
let tracks: { index: number; title: string; group: string; duration: string; encoding: string }[] = [];
|
let tracks: DisplayTrack[] = [];
|
||||||
if (disc !== null) {
|
if (disc !== null) {
|
||||||
for (let group of disc.groups) {
|
for (let group of disc.groups) {
|
||||||
for (let track of group.tracks) {
|
for (let track of group.tracks) {
|
||||||
tracks.push({
|
tracks.push({
|
||||||
index: track.index,
|
index: track.index,
|
||||||
title: track.title ?? `Unknown Title`,
|
title: track.title ?? `Unknown Title`,
|
||||||
group: group.title ?? ``,
|
fullWidthTitle: track.fullWidthTitle ?? ``,
|
||||||
|
group: group.title ?? null,
|
||||||
encoding: EncodingName[track.encoding],
|
encoding: EncodingName[track.encoding],
|
||||||
duration: formatTimeFromFrames(track.duration, false),
|
duration: formatTimeFromFrames(track.duration, false),
|
||||||
});
|
});
|
||||||
|
@ -90,6 +170,178 @@ export function getSortedTracks(disc: Disc | null) {
|
||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getGroupedTracks(disc: Disc | null) {
|
||||||
|
if (!disc) return [];
|
||||||
|
let groupedList: Group[] = [];
|
||||||
|
let ungroupedTracks = [...(disc.groups.find(n => n.title === null)?.tracks ?? [])];
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
for (let group of disc.groups) {
|
||||||
|
if (group.title === null) continue; // Ungrouped tracks
|
||||||
|
let toCopy = group.tracks[0].index - lastIndex;
|
||||||
|
groupedList.push({
|
||||||
|
index: -1,
|
||||||
|
title: null,
|
||||||
|
fullWidthTitle: null,
|
||||||
|
tracks: toCopy === 0 ? [] : ungroupedTracks.splice(0, toCopy),
|
||||||
|
});
|
||||||
|
lastIndex = group.tracks[group.tracks.length - 1].index + 1;
|
||||||
|
groupedList.push(group);
|
||||||
|
}
|
||||||
|
groupedList.push({
|
||||||
|
index: -1,
|
||||||
|
title: null,
|
||||||
|
fullWidthTitle: null,
|
||||||
|
tracks: ungroupedTracks,
|
||||||
|
});
|
||||||
|
return groupedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recomputeGroupsAfterTrackMove(disc: Disc, trackIndex: number, targetIndex: number) {
|
||||||
|
// Used for moving tracks in netmd-mock and deleting
|
||||||
|
let offset = trackIndex > targetIndex ? 1 : -1;
|
||||||
|
let deleteMode = targetIndex === -1;
|
||||||
|
|
||||||
|
if (deleteMode) {
|
||||||
|
offset = -1;
|
||||||
|
targetIndex = disc.trackCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
let boundsStart = Math.min(trackIndex, targetIndex);
|
||||||
|
let boundsEnd = Math.max(trackIndex, targetIndex);
|
||||||
|
|
||||||
|
let allTracks = disc.groups
|
||||||
|
.map(n => n.tracks)
|
||||||
|
.reduce((a, b) => a.concat(b), [])
|
||||||
|
.sort((a, b) => a.index - b.index)
|
||||||
|
.filter(n => !deleteMode || n.index !== trackIndex);
|
||||||
|
|
||||||
|
let groupBoundaries: {
|
||||||
|
name: string | null;
|
||||||
|
fullWidthName: string | null;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}[] = disc.groups
|
||||||
|
.filter(n => n.title !== null)
|
||||||
|
.map(group => ({
|
||||||
|
name: group.title,
|
||||||
|
fullWidthName: group.fullWidthTitle,
|
||||||
|
start: group.tracks[0].index,
|
||||||
|
end: group.tracks[0].index + group.tracks.length - 1,
|
||||||
|
})); // Convert to a format better for shifting
|
||||||
|
|
||||||
|
let anyChanges = false;
|
||||||
|
|
||||||
|
for (let group of groupBoundaries) {
|
||||||
|
if (group.start > boundsStart && group.start <= boundsEnd) {
|
||||||
|
group.start += offset;
|
||||||
|
anyChanges = true;
|
||||||
|
}
|
||||||
|
if (group.end >= boundsStart && group.end < boundsEnd) {
|
||||||
|
group.end += offset;
|
||||||
|
anyChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyChanges) return disc;
|
||||||
|
|
||||||
|
let newDisc: Disc = { ...disc };
|
||||||
|
|
||||||
|
// Convert back
|
||||||
|
newDisc.groups = groupBoundaries
|
||||||
|
.map(n => ({
|
||||||
|
title: n.name,
|
||||||
|
fullWidthTitle: n.fullWidthName,
|
||||||
|
index: n.start,
|
||||||
|
tracks: allTracks.slice(n.start, n.end + 1),
|
||||||
|
}))
|
||||||
|
.filter(n => n.tracks.length > 0);
|
||||||
|
|
||||||
|
// Convert ungrouped tracks
|
||||||
|
let allGrouped = newDisc.groups.map(n => n.tracks).reduce((a, b) => a.concat(b), []);
|
||||||
|
let ungrouped = allTracks.filter(n => !allGrouped.includes(n));
|
||||||
|
|
||||||
|
// Fix all the track indexes
|
||||||
|
if (deleteMode) {
|
||||||
|
for (let i = 0; i < allTracks.length; i++) {
|
||||||
|
allTracks[i].index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ungrouped.length) newDisc.groups.unshift({ title: null, fullWidthTitle: null, index: 0, tracks: ungrouped });
|
||||||
|
|
||||||
|
return newDisc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileDiscTitles(disc: Disc) {
|
||||||
|
let availableCharactersForTitle = getAvailableCharsForTitle(
|
||||||
|
{
|
||||||
|
...disc,
|
||||||
|
title: '',
|
||||||
|
fullWidthTitle: '',
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
// If the disc or any of the groups, or any track has a full-width title, provide support for them
|
||||||
|
const useFullWidth =
|
||||||
|
disc.fullWidthTitle ||
|
||||||
|
disc.groups.filter(n => !!n.fullWidthTitle).length > 0 ||
|
||||||
|
disc.groups
|
||||||
|
.map(n => n.tracks)
|
||||||
|
.reduce((a, b) => a.concat(b), [])
|
||||||
|
.filter(n => !!n.fullWidthTitle).length > 0;
|
||||||
|
|
||||||
|
const fixLength = (l: number) => Math.ceil(l / 7) * 7;
|
||||||
|
|
||||||
|
let newRawTitle = '',
|
||||||
|
newRawFullWidthTitle = '';
|
||||||
|
if (disc.title) newRawTitle = `0;${disc.title}//`;
|
||||||
|
if (useFullWidth) newRawFullWidthTitle = `0;${disc.fullWidthTitle}//`;
|
||||||
|
for (let n of disc.groups) {
|
||||||
|
if (n.title === null || n.tracks.length === 0) continue;
|
||||||
|
let range = `${n.tracks[0].index + 1}`;
|
||||||
|
if (n.tracks.length !== 1) {
|
||||||
|
// Special case
|
||||||
|
range += `-${n.tracks[0].index + n.tracks.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newRawTitleAfterGroup = newRawTitle + `${range};${n.title}//`,
|
||||||
|
newRawFullWidthTitleAfterGroup = newRawFullWidthTitle + halfWidthToFullWidthRange(range) + `;${n.fullWidthTitle ?? ''}//`;
|
||||||
|
|
||||||
|
let titlesLengthInTOC = fixLength(getHalfWidthTitleLength(newRawTitleAfterGroup));
|
||||||
|
|
||||||
|
if (useFullWidth) titlesLengthInTOC += fixLength(newRawFullWidthTitleAfterGroup.length * 2);
|
||||||
|
|
||||||
|
if (availableCharactersForTitle - titlesLengthInTOC < 0) break;
|
||||||
|
|
||||||
|
newRawTitle = newRawTitleAfterGroup;
|
||||||
|
newRawFullWidthTitle = newRawFullWidthTitleAfterGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
let titlesLengthInTOC = fixLength(getHalfWidthTitleLength(newRawTitle));
|
||||||
|
if (useFullWidth) titlesLengthInTOC += fixLength(newRawFullWidthTitle.length * 2); // If this check fails the titles without the groups already take too much space, don't change anything
|
||||||
|
if (availableCharactersForTitle - titlesLengthInTOC < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newRawTitle,
|
||||||
|
newRawFullWidthTitle: useFullWidth ? newRawFullWidthTitle : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSequential(numbers: number[]) {
|
||||||
|
if (numbers.length === 0) return true;
|
||||||
|
let last = numbers[0];
|
||||||
|
for (let num of numbers) {
|
||||||
|
if (num === last) {
|
||||||
|
++last;
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function asyncMutex(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
export function asyncMutex(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
// This is meant to be used only with classes having a "mutex" instance property
|
// This is meant to be used only with classes having a "mutex" instance property
|
||||||
const oldValue = descriptor.value;
|
const oldValue = descriptor.value;
|
||||||
|
|
77
yarn.lock
77
yarn.lock
|
@ -877,6 +877,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"regenerator-runtime" "^0.13.4"
|
"regenerator-runtime" "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.9.2":
|
||||||
|
"integrity" "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg=="
|
||||||
|
"resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz"
|
||||||
|
"version" "7.14.6"
|
||||||
|
dependencies:
|
||||||
|
"regenerator-runtime" "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.4.0", "@babel/template@^7.8.3":
|
"@babel/template@^7.4.0", "@babel/template@^7.8.3":
|
||||||
"integrity" "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ=="
|
"integrity" "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ=="
|
||||||
"resolved" "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz"
|
"resolved" "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz"
|
||||||
|
@ -1591,6 +1598,13 @@
|
||||||
"resolved" "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz"
|
"resolved" "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz"
|
||||||
"version" "1.5.2"
|
"version" "1.5.2"
|
||||||
|
|
||||||
|
"@types/react-beautiful-dnd@^13.1.1":
|
||||||
|
"integrity" "sha512-1lBBxVSutE8CQM37Jq7KvJwuA94qaEEqsx+G0dnwzG6Sfwf6JGcNeFk5jjjhJli1q2naeMZm+D/dvT/zyX4QPw=="
|
||||||
|
"resolved" "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz"
|
||||||
|
"version" "13.1.1"
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-dom@*", "@types/react-dom@^16.9.10":
|
"@types/react-dom@*", "@types/react-dom@^16.9.10":
|
||||||
"integrity" "sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw=="
|
"integrity" "sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw=="
|
||||||
"resolved" "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz"
|
"resolved" "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz"
|
||||||
|
@ -3554,6 +3568,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"postcss" "^7.0.5"
|
"postcss" "^7.0.5"
|
||||||
|
|
||||||
|
"css-box-model@^1.2.0":
|
||||||
|
"integrity" "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw=="
|
||||||
|
"resolved" "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz"
|
||||||
|
"version" "1.2.1"
|
||||||
|
dependencies:
|
||||||
|
"tiny-invariant" "^1.0.6"
|
||||||
|
|
||||||
"css-color-keywords@^1.0.0":
|
"css-color-keywords@^1.0.0":
|
||||||
"integrity" "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
|
"integrity" "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
|
||||||
"resolved" "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz"
|
"resolved" "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz"
|
||||||
|
@ -7401,6 +7422,11 @@
|
||||||
"mimic-fn" "^2.0.0"
|
"mimic-fn" "^2.0.0"
|
||||||
"p-is-promise" "^2.0.0"
|
"p-is-promise" "^2.0.0"
|
||||||
|
|
||||||
|
"memoize-one@^5.1.1":
|
||||||
|
"integrity" "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||||
|
"resolved" "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz"
|
||||||
|
"version" "5.2.1"
|
||||||
|
|
||||||
"memory-fs@^0.4.1":
|
"memory-fs@^0.4.1":
|
||||||
"integrity" "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI="
|
"integrity" "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI="
|
||||||
"resolved" "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz"
|
"resolved" "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz"
|
||||||
|
@ -7796,10 +7822,10 @@
|
||||||
"resolved" "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz"
|
"resolved" "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz"
|
||||||
"version" "2.6.1"
|
"version" "2.6.1"
|
||||||
|
|
||||||
"netmd-js@^1.4.0":
|
"netmd-js@^2.0.1":
|
||||||
"integrity" "sha512-8OXEO4Gh6Me3XU2igHmVbk+hn06O3N5xlM/c38w97rNQDRpCAppO8jqq+fryaZd7QZwmIxXqwwY88KIehoieAg=="
|
"integrity" "sha512-9rMt+tcpqYM1/IyudyzVeOzZ+E55/+RvHf6NNOzQJbb0knySG5oaLich2NXSsFT45Wrg+c43uLMu3iOKmjW4aA=="
|
||||||
"resolved" "https://registry.npmjs.org/netmd-js/-/netmd-js-1.4.1.tgz"
|
"resolved" "https://registry.npmjs.org/netmd-js/-/netmd-js-2.0.1.tgz"
|
||||||
"version" "1.4.1"
|
"version" "2.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"browser-bunyan" "^1.5.3"
|
"browser-bunyan" "^1.5.3"
|
||||||
"buffer" "^5.5.0"
|
"buffer" "^5.5.0"
|
||||||
|
@ -9583,6 +9609,11 @@
|
||||||
"resolved" "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz"
|
"resolved" "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz"
|
||||||
"version" "2.1.1"
|
"version" "2.1.1"
|
||||||
|
|
||||||
|
"raf-schd@^4.0.2":
|
||||||
|
"integrity" "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||||
|
"resolved" "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz"
|
||||||
|
"version" "4.0.3"
|
||||||
|
|
||||||
"raf@^3.4.1":
|
"raf@^3.4.1":
|
||||||
"integrity" "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="
|
"integrity" "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="
|
||||||
"resolved" "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz"
|
"resolved" "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz"
|
||||||
|
@ -9642,6 +9673,19 @@
|
||||||
"regenerator-runtime" "^0.13.3"
|
"regenerator-runtime" "^0.13.3"
|
||||||
"whatwg-fetch" "^3.0.0"
|
"whatwg-fetch" "^3.0.0"
|
||||||
|
|
||||||
|
"react-beautiful-dnd@^13.1.0":
|
||||||
|
"integrity" "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA=="
|
||||||
|
"resolved" "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz"
|
||||||
|
"version" "13.1.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.9.2"
|
||||||
|
"css-box-model" "^1.2.0"
|
||||||
|
"memoize-one" "^5.1.1"
|
||||||
|
"raf-schd" "^4.0.2"
|
||||||
|
"react-redux" "^7.2.0"
|
||||||
|
"redux" "^4.0.4"
|
||||||
|
"use-memo-one" "^1.1.1"
|
||||||
|
|
||||||
"react-dev-utils@^10.1.0":
|
"react-dev-utils@^10.1.0":
|
||||||
"integrity" "sha512-KmZChqxY6l+ed28IHetGrY8J9yZSvzlAHyFXduEIhQ42EBGtqftlbqQZ+dDTaC7CwNW2tuXN+66bRKE5h2HgrQ=="
|
"integrity" "sha512-KmZChqxY6l+ed28IHetGrY8J9yZSvzlAHyFXduEIhQ42EBGtqftlbqQZ+dDTaC7CwNW2tuXN+66bRKE5h2HgrQ=="
|
||||||
"resolved" "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.1.0.tgz"
|
"resolved" "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.1.0.tgz"
|
||||||
|
@ -9672,7 +9716,7 @@
|
||||||
"strip-ansi" "6.0.0"
|
"strip-ansi" "6.0.0"
|
||||||
"text-table" "0.2.0"
|
"text-table" "0.2.0"
|
||||||
|
|
||||||
"react-dom@^16.14.0":
|
"react-dom@^16.14.0", "react-dom@^16.8.5 || ^17.0.0":
|
||||||
"integrity" "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw=="
|
"integrity" "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw=="
|
||||||
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz"
|
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz"
|
||||||
"version" "16.14.0"
|
"version" "16.14.0"
|
||||||
|
@ -9706,7 +9750,7 @@
|
||||||
"resolved" "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
"resolved" "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
"version" "16.13.1"
|
"version" "16.13.1"
|
||||||
|
|
||||||
"react-redux@^7.2.2":
|
"react-redux@^7.2.0", "react-redux@^7.2.2":
|
||||||
"integrity" "sha512-ZhAmQ1lrK+Pyi0ZXNMUZuYxYAZd59wFuVDGUt536kSGdD0ya9Q7BfsE95E3TsFLE3kOSFp5m6G5qbatE+Ic1+w=="
|
"integrity" "sha512-ZhAmQ1lrK+Pyi0ZXNMUZuYxYAZd59wFuVDGUt536kSGdD0ya9Q7BfsE95E3TsFLE3kOSFp5m6G5qbatE+Ic1+w=="
|
||||||
"resolved" "https://registry.npmjs.org/react-redux/-/react-redux-7.2.3.tgz"
|
"resolved" "https://registry.npmjs.org/react-redux/-/react-redux-7.2.3.tgz"
|
||||||
"version" "7.2.3"
|
"version" "7.2.3"
|
||||||
|
@ -9788,7 +9832,7 @@
|
||||||
"loose-envify" "^1.4.0"
|
"loose-envify" "^1.4.0"
|
||||||
"prop-types" "^15.6.2"
|
"prop-types" "^15.6.2"
|
||||||
|
|
||||||
"react@^16.14.0", "react@^16.8.3 || ^17":
|
"react@^16.14.0", "react@^16.8.0 || ^17.0.0", "react@^16.8.3 || ^17", "react@^16.8.5 || ^17.0.0":
|
||||||
"integrity" "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g=="
|
"integrity" "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g=="
|
||||||
"resolved" "https://registry.npmjs.org/react/-/react-16.14.0.tgz"
|
"resolved" "https://registry.npmjs.org/react/-/react-16.14.0.tgz"
|
||||||
"version" "16.14.0"
|
"version" "16.14.0"
|
||||||
|
@ -10045,7 +10089,7 @@
|
||||||
"resolved" "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz"
|
"resolved" "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz"
|
||||||
"version" "2.3.0"
|
"version" "2.3.0"
|
||||||
|
|
||||||
"redux@^2.0.0 || ^3.0.0 || ^4.0.0-0", "redux@^4.0.0":
|
"redux@^2.0.0 || ^3.0.0 || ^4.0.0-0", "redux@^4.0.0", "redux@^4.0.4":
|
||||||
"integrity" "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w=="
|
"integrity" "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w=="
|
||||||
"resolved" "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz"
|
"resolved" "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz"
|
||||||
"version" "4.0.5"
|
"version" "4.0.5"
|
||||||
|
@ -10070,12 +10114,7 @@
|
||||||
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
|
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz"
|
||||||
"version" "0.11.1"
|
"version" "0.11.1"
|
||||||
|
|
||||||
"regenerator-runtime@^0.13.2", "regenerator-runtime@^0.13.3":
|
"regenerator-runtime@^0.13.2", "regenerator-runtime@^0.13.3", "regenerator-runtime@^0.13.4":
|
||||||
"integrity" "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
|
|
||||||
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz"
|
|
||||||
"version" "0.13.3"
|
|
||||||
|
|
||||||
"regenerator-runtime@^0.13.4":
|
|
||||||
"integrity" "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
"integrity" "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||||
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz"
|
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz"
|
||||||
"version" "0.13.7"
|
"version" "0.13.7"
|
||||||
|
@ -11486,6 +11525,11 @@
|
||||||
"resolved" "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz"
|
"resolved" "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz"
|
||||||
"version" "0.3.0"
|
"version" "0.3.0"
|
||||||
|
|
||||||
|
"tiny-invariant@^1.0.6":
|
||||||
|
"integrity" "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
|
||||||
|
"resolved" "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz"
|
||||||
|
"version" "1.1.0"
|
||||||
|
|
||||||
"tiny-warning@^1.0.2":
|
"tiny-warning@^1.0.2":
|
||||||
"integrity" "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
"integrity" "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||||
"resolved" "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
|
"resolved" "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
|
||||||
|
@ -11807,6 +11851,11 @@
|
||||||
"nan" "2.13.2"
|
"nan" "2.13.2"
|
||||||
"prebuild-install" "^5.3.3"
|
"prebuild-install" "^5.3.3"
|
||||||
|
|
||||||
|
"use-memo-one@^1.1.1":
|
||||||
|
"integrity" "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ=="
|
||||||
|
"resolved" "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz"
|
||||||
|
"version" "1.1.2"
|
||||||
|
|
||||||
"use@^3.1.0":
|
"use@^3.1.0":
|
||||||
"integrity" "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
"integrity" "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
||||||
"resolved" "https://registry.npmjs.org/use/-/use-3.1.1.tgz"
|
"resolved" "https://registry.npmjs.org/use/-/use-3.1.1.tgz"
|
||||||
|
|
Loading…
Reference in New Issue