mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.20.59
* Added: Support for different settings providers, including IndexedDB. * Fixed: Alignment of descriptions with check box settings. * Fixed: Settings being added multiple times to the Control Center UI when registered multiple times. * API Added: Providers now support blobs, and emit events when blobs change. * Maintenance: Update dependencies. Add a babel plugin.
This commit is contained in:
parent
b337b6abe3
commit
5412a928a1
12 changed files with 976 additions and 96 deletions
3
.babelrc
3
.babelrc
|
@ -3,6 +3,7 @@
|
|||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
["@babel/plugin-proposal-object-rest-spread", {"loose": true, "useBuiltIns": true}]
|
||||
["@babel/plugin-proposal-object-rest-spread", {"loose": true, "useBuiltIns": true}],
|
||||
["@babel/plugin-proposal-class-properties", {"loose": true}]
|
||||
]
|
||||
}
|
232
package-lock.json
generated
232
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"version": "4.20.31",
|
||||
"version": "4.20.58",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -102,6 +102,162 @@
|
|||
"@babel/types": "^7.10.4"
|
||||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.13.tgz",
|
||||
"integrity": "sha512-Vs/e9wv7rakKYeywsmEBSRC9KtmE7Px+YBlESekLeJOF0zbGUicGfXSNi3o+tfXSNS48U/7K9mIOOCR79Cl3+Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.13",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.13",
|
||||
"@babel/helper-replace-supers": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.12.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz",
|
||||
"integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.13",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz",
|
||||
"integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "^7.12.13",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-get-function-arity": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz",
|
||||
"integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-member-expression-to-functions": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz",
|
||||
"integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz",
|
||||
"integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz",
|
||||
"integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.13",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.13",
|
||||
"@babel/traverse": "^7.12.13",
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz",
|
||||
"integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz",
|
||||
"integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz",
|
||||
"integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
|
||||
"integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/parser": "^7.12.13",
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz",
|
||||
"integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.12.13",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.12.13",
|
||||
"@babel/types": "^7.12.13",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz",
|
||||
"integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"lodash": "^4.17.19",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz",
|
||||
|
@ -235,6 +391,24 @@
|
|||
"integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.13.tgz",
|
||||
"integrity": "sha512-8SCJ0Ddrpwv4T7Gwb33EmW1V9PY5lggTO+A8WjyIwxrSHDUyBw4MtF96ifn1n8H806YlxbVCoKXbbmzD6RD+cA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.12.13",
|
||||
"@babel/helper-plugin-utils": "^7.12.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz",
|
||||
"integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
|
||||
|
@ -373,9 +547,9 @@
|
|||
}
|
||||
},
|
||||
"@ffz/fontello-cli": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ffz/fontello-cli/-/fontello-cli-1.0.3.tgz",
|
||||
"integrity": "sha512-xdXNiaT5+wgnYWCrX5thonncE5bN9fFk+z5rGxr6AwCNXQQKzg2D24MngDY3DTT5LHRwLid3CQJ2GHJ5g9n0FQ==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@ffz/fontello-cli/-/fontello-cli-1.0.4.tgz",
|
||||
"integrity": "sha512-h4D7/gi7gUR7mDy4vopfjzca1uCx1TGiM8K4d9sxWMeRMEbLdpAb/Lxz5awLr+UjXEjxios2bvpgXZ1BiA/CNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colors": "^1.4.0",
|
||||
|
@ -1383,9 +1557,9 @@
|
|||
}
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.45",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.45.tgz",
|
||||
"integrity": "sha512-nmb9E7oEtVJ7SmSCH/DeJobXyuRmaofkpoQSimMFu3HKJ5MADtM825SPLhDuWhZ6TElLAQtgJbQmBZuHIRlZoA==",
|
||||
"version": "1.6.48",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
||||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==",
|
||||
"dev": true
|
||||
},
|
||||
"big.js": {
|
||||
|
@ -1668,9 +1842,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"buffer-indexof-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz",
|
||||
"integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
|
||||
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-xor": {
|
||||
|
@ -4591,9 +4765,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true
|
||||
},
|
||||
"internal-ip": {
|
||||
|
@ -5720,15 +5894,15 @@
|
|||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
||||
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
||||
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
||||
"dev": true
|
||||
},
|
||||
"node-gyp": {
|
||||
|
@ -7442,12 +7616,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"selfsigned": {
|
||||
"version": "1.10.7",
|
||||
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
|
||||
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
|
||||
"version": "1.10.8",
|
||||
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
|
||||
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-forge": "0.9.0"
|
||||
"node-forge": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
|
@ -8971,9 +9145,9 @@
|
|||
}
|
||||
},
|
||||
"unzipper": {
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.5.tgz",
|
||||
"integrity": "sha512-i5ufkXNjWZYxU/0nKKf6LkvW8kn9YzRvfwuPWjXP+JTFce/8bqeR0gEfbiN2IDdJa6ZU6/2IzFRLK0z1v0uptw==",
|
||||
"version": "0.10.11",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
|
||||
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big-integer": "^1.6.17",
|
||||
|
@ -8995,9 +9169,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
|
||||
"integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==",
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.58",
|
||||
"version": "4.20.59",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
@ -25,12 +25,13 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.13",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.10.4",
|
||||
"@ffz/fontello-cli": "^1.0.3",
|
||||
"@ffz/fontello-cli": "^1.0.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
|
|
147
src/modules/main_menu/components/provider.vue
Normal file
147
src/modules/main_menu/components/provider.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--provider tw-pd-t-05">
|
||||
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('setting.provider.warn.title', 'Be careful!') }}
|
||||
</h3>
|
||||
<div>
|
||||
<markdown :source="t('setting.provider.warn.desc', 'Please close any other Twitch tabs before using this tool. It is **recommended to create a backup** before changing your provider, in case anything happens.')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="tw-pd-b-1 tw-mg-b-1 tw-border-b">
|
||||
<markdown :source="t('setting.provider.about', 'Here, you can change the storage provider used by FrankerFaceZ when browsing Twitch. You can try this if you\'re having a problem with your settings not remaining persistent. Please note that you will need to reload any and all Twitch tabs after changing this.')" />
|
||||
</section>
|
||||
|
||||
<div class="ffz-options">
|
||||
<div v-for="val of providers" :key="val.key" class="tw-pd-b-1 tw-radio ffz-radio-top">
|
||||
<input
|
||||
:id="'ffz--provider-opt-' + val.key"
|
||||
v-model="selected"
|
||||
:value="val.key"
|
||||
name="ffz--provider-opt"
|
||||
type="radio"
|
||||
class="tw-radio__input"
|
||||
>
|
||||
<label
|
||||
:for="'ffz--provider-opt-' + val.key"
|
||||
class="tw-block tw-radio__label"
|
||||
>
|
||||
<div class="tw-mg-l-1">
|
||||
<div>
|
||||
<span class="tw-strong">
|
||||
{{ t(val.i18n_key, val.title) }}
|
||||
</span>
|
||||
<span v-if="val.key === current" class="tw-mg-l-1 tw-c-text-alt">
|
||||
{{ t('setting.provider.selected', '(Current)') }}
|
||||
</span>
|
||||
<span v-if="val.has_data" class="tw-mg-l-1 tw-c-text-alt">
|
||||
{{ t('setting.provider.has-data', '(Has Data)') }}
|
||||
</span>
|
||||
</div>
|
||||
<section v-if="val.description" class="tw-c-text-alt-2">
|
||||
<markdown :source="t(val.desc_i18n_key, val.description)" />
|
||||
</section>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-border-t tw-pd-t-1">
|
||||
<div class="tw-flex tw-align-items-center tw-checkbox">
|
||||
<input id="transfer" ref="transfer" checked type="checkbox" class="tw-checkbox__input">
|
||||
<label for="transfer" class="tw-checkbox__label">
|
||||
<div class="tw-mg-l-1">
|
||||
{{ t('setting.provider.transfer', 'Transfer my settings when switching provider.') }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<section class="tw-c-text-alt-2" style="padding-left:2.5rem">
|
||||
<markdown :source="t('setting.provider.transfer.desc', '**Note:** It is recommended to leave this enabled unless you know what you\'re doing.')" />
|
||||
</section>
|
||||
<div class="tw-mg-t-1 tw-flex tw-align-items-center tw-checkbox">
|
||||
<input id="backup" ref="backup" v-model="backup" type="checkbox" class="tw-checkbox__input">
|
||||
<label for="backup" class="tw-checkbox__label">
|
||||
<div class="tw-mg-l-1">
|
||||
{{ t('setting.provider.backup', 'Yes, I made a backup.') }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
||||
<button
|
||||
class="tw-button tw-mg-l-3"
|
||||
:class="{'tw-button--disabled': ! enabled}"
|
||||
@click="change"
|
||||
>
|
||||
<span class="tw-button__icon tw-button__icon--left">
|
||||
<figure class="ffz-i-floppy" />
|
||||
</span>
|
||||
<span class="tw-button__text">
|
||||
{{ t('setting.provider.start', 'Change Providers') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
const ffz = this.context.getFFZ(),
|
||||
settings = ffz.resolve('settings'),
|
||||
providers = [];
|
||||
|
||||
for(const [key, val] of Object.entries(settings.getProviders())) {
|
||||
const prov = {
|
||||
key,
|
||||
has_data: null,
|
||||
i18n_key: `setting.provider.${key}.title`,
|
||||
title: val.title || key,
|
||||
desc_i18n_key: val.description ? `setting.provider.${key}.desc` : null,
|
||||
description: val.description
|
||||
};
|
||||
|
||||
if ( val.supported() )
|
||||
Promise.resolve(val.hasContent()).then(v => {
|
||||
prov.has_data = v;
|
||||
});
|
||||
|
||||
providers.push(prov);
|
||||
|
||||
}
|
||||
|
||||
const current = settings.getActiveProvider();
|
||||
|
||||
return {
|
||||
backup: false,
|
||||
providers,
|
||||
current,
|
||||
selected: current
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
enabled() {
|
||||
return this.selected !== this.current && this.backup
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
change() {
|
||||
if ( ! this.enabled )
|
||||
return;
|
||||
|
||||
const ffz = this.context.getFFZ(),
|
||||
settings = ffz.resolve('settings');
|
||||
|
||||
settings.changeProvider(this.selected, this.$refs.transfer.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -50,13 +50,13 @@
|
|||
<section
|
||||
v-if="item.description"
|
||||
class="tw-c-text-alt-2"
|
||||
style="padding-left:2.2rem"
|
||||
style="padding-left:2.5rem"
|
||||
>
|
||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||
</section>
|
||||
<section
|
||||
v-if="item.extra"
|
||||
style="padding-left:2.2rem"
|
||||
style="padding-left:2.5rem"
|
||||
>
|
||||
<component :is="item.extra.component" :context="context" :item="item" />
|
||||
</section>
|
||||
|
|
|
@ -66,6 +66,12 @@ export default class MainMenu extends Module {
|
|||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.addUI('provider', {
|
||||
path: 'Data Management > Storage >> tabs ~> Provider',
|
||||
component: 'provider',
|
||||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.addUI('home', {
|
||||
path: 'Home @{"sort": -1000, "profile_warning": false}',
|
||||
component: 'home-page'
|
||||
|
|
|
@ -63,6 +63,9 @@ export const Everything = {
|
|||
label: 'Absolutely Everything',
|
||||
clear(provider, settings) {
|
||||
provider.clear();
|
||||
if ( provider.supportsBlobs() )
|
||||
provider.clearBlobs();
|
||||
|
||||
settings.loadProfiles();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
import Module from 'utilities/module';
|
||||
import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
|
||||
|
||||
import {IndexedDBProvider, LocalStorageProvider} from './providers';
|
||||
import SettingsProfile from './profile';
|
||||
import SettingsContext from './context';
|
||||
import MigrationManager from './migration';
|
||||
|
||||
import * as PROVIDERS from './providers';
|
||||
import * as FILTERS from './filters';
|
||||
import * as CLEARABLES from './clearables';
|
||||
|
||||
|
@ -33,6 +33,18 @@ export default class SettingsManager extends Module {
|
|||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.providers = {};
|
||||
for(const key in PROVIDERS)
|
||||
if ( has(PROVIDERS, key) ) {
|
||||
const provider = PROVIDERS[key];
|
||||
if ( provider.key && provider.supported() )
|
||||
this.providers[provider.key] = provider;
|
||||
}
|
||||
|
||||
// This cannot be modified at a future time, as providers NEED
|
||||
// to be ready very early in FFZ intitialization. Seal it.
|
||||
Object.seal(this.providers);
|
||||
|
||||
this.updateSoon = debounce(() => this.updateRoutes(), 50, false);
|
||||
|
||||
// Do we want to not enable any profiles?
|
||||
|
@ -68,9 +80,19 @@ export default class SettingsManager extends Module {
|
|||
|
||||
|
||||
// Create our provider as early as possible.
|
||||
const provider = this.provider = this._createProvider();
|
||||
this.log.info(`Using Provider: ${provider.constructor.name}`);
|
||||
provider.on('changed', this._onProviderChange, this);
|
||||
this._provider_waiters = [];
|
||||
|
||||
this._createProvider().then(provider => {
|
||||
this.provider = provider;
|
||||
this.log.info(`Using Provider: ${provider.constructor.name}`);
|
||||
provider.on('changed', this._onProviderChange, this);
|
||||
provider.on('change-provider', () => {
|
||||
this.emit(':change-provider');
|
||||
});
|
||||
|
||||
for(const waiter of this._provider_waiters)
|
||||
waiter(provider);
|
||||
});
|
||||
|
||||
this.migrations = new MigrationManager(this);
|
||||
|
||||
|
@ -113,11 +135,22 @@ export default class SettingsManager extends Module {
|
|||
return out.join('\n');
|
||||
}
|
||||
|
||||
awaitProvider() {
|
||||
if ( this.provider )
|
||||
return Promise.resolve(this.provider);
|
||||
|
||||
return new Promise(s => {
|
||||
this._provider_waiters.push(s);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when the SettingsManager instance should be enabled.
|
||||
*/
|
||||
async onEnable() {
|
||||
// Before we do anything else, make sure the provider is ready.
|
||||
await this.awaitProvider();
|
||||
await this.provider.awaitReady();
|
||||
|
||||
// When the router updates we additional routes, make sure to
|
||||
|
@ -232,21 +265,127 @@ export default class SettingsManager extends Module {
|
|||
// Provider Interaction
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Return an object with all the known, supported providers.
|
||||
* @returns {Object} The object.
|
||||
*/
|
||||
getProviders() {
|
||||
return this.providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the active provider.
|
||||
* @returns {String} The key for the active provider
|
||||
*/
|
||||
getActiveProvider() {
|
||||
return this._active_provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the environment that FFZ is running in and then decide which
|
||||
* provider should be used to retrieve and store settings.
|
||||
*
|
||||
* @returns {SettingsProvider} The provider to store everything.
|
||||
*/
|
||||
_createProvider() {
|
||||
// Prefer IndexedDB if it's available because it's more persistent
|
||||
// and can store more data. Plus, we don't have to faff around with
|
||||
// JSON conversion all the time.
|
||||
if ( IndexedDBProvider.supported() && localStorage.ffzIDB )
|
||||
return this._idb = new IndexedDBProvider(this);
|
||||
async _createProvider() {
|
||||
let wanted = localStorage.ffzProvider;
|
||||
if ( wanted == null )
|
||||
wanted = localStorage.ffzProvider = await this.sniffProvider();
|
||||
|
||||
// Fallback
|
||||
return new LocalStorageProvider(this);
|
||||
if ( this.providers[wanted] ) {
|
||||
const provider = new this.providers[wanted](this);
|
||||
if ( wanted === 'idb' )
|
||||
this._idb = provider;
|
||||
|
||||
this._active_provider = wanted;
|
||||
return provider;
|
||||
}
|
||||
|
||||
// Fallback to localStorage if nothing else was wanted and available.
|
||||
this._active_provider = 'local';
|
||||
return new this.providers.local(this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluate the environment and attempt to guess which provider we should
|
||||
* use for storing settings. This is necessary in case localStorage is
|
||||
* cleared while we have settings stored in IndexedDB.
|
||||
*
|
||||
* In the future, this may default to IndexedDB for new users.
|
||||
*
|
||||
* @returns {String} The key for which provider we should use.
|
||||
*/
|
||||
async sniffProvider() {
|
||||
const providers = Object.values(this.providers);
|
||||
providers.sort((a,b) => b.priority - a.priority);
|
||||
|
||||
for(const provider of providers) {
|
||||
if ( provider.supported() && provider.hasContent && await provider.hasContent() ) // eslint-disable-line no-await-in-loop
|
||||
return provider.key;
|
||||
}
|
||||
|
||||
// Fallback to local if no provider indicated present settings.
|
||||
return 'local';
|
||||
}
|
||||
|
||||
/**
|
||||
* Change to a new settings provider. This immediately prevents changes
|
||||
* to the old provider, and will reload the page when settings have
|
||||
* been transfered over.
|
||||
*
|
||||
* @param {String} key The key of the new provider to swap to.
|
||||
* @param {Boolean} transfer Whether or not settings should be transferred
|
||||
* from the current provider.
|
||||
*/
|
||||
async changeProvider(key, transfer) {
|
||||
if ( ! this.providers[key] || ! this.providers[key].supported() )
|
||||
throw new Error(`Invalid provider: ${key}`);
|
||||
|
||||
// If we're changing to the current provider... well, that doesn't make
|
||||
// a lot of sense, does it? Abort!
|
||||
if ( key === this._active_provider )
|
||||
return;
|
||||
|
||||
const old_provider = this.provider;
|
||||
this.provider = null;
|
||||
|
||||
// Let all other tabs know what's up.
|
||||
old_provider.broadcastTransfer();
|
||||
|
||||
// Are we transfering settings?
|
||||
if ( transfer ) {
|
||||
const new_provider = new this.providers[key](this);
|
||||
|
||||
old_provider.disableEvents();
|
||||
|
||||
// When transfering, we clear all existing settings.
|
||||
await new_provider.clear();
|
||||
if ( new_provider.supportsBlobs )
|
||||
await new_provider.clearBlobs();
|
||||
|
||||
for(const [key,val] of old_provider.entries())
|
||||
new_provider.set(key, val);
|
||||
|
||||
if ( old_provider.supportsBlobs && new_provider.supportsBlobs ) {
|
||||
for(const key of await old_provider.blobKeys() ) {
|
||||
const blob = await old_provider.getBlob(key); // eslint-disable-line no-await-in-loop
|
||||
if ( blob )
|
||||
await new_provider.setBlob(key, blob); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
await old_provider.clearBlobs();
|
||||
}
|
||||
|
||||
old_provider.clear();
|
||||
|
||||
await old_provider.flush();
|
||||
await new_provider.flush();
|
||||
}
|
||||
|
||||
// Change over.
|
||||
localStorage.ffzProvider = key;
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
|
@ -587,7 +726,11 @@ export default class SettingsManager extends Module {
|
|||
this.on(`:changed:${key}`, definition.changed);
|
||||
|
||||
this.definitions.set(key, definition);
|
||||
this.emit(':added-definition', key, definition);
|
||||
|
||||
// Do not re-emit `added-definition` when re-adding an existing
|
||||
// setting. Prevents the settings UI from goofing up.
|
||||
if ( ! old_definition || Array.isArray(old_definition) )
|
||||
this.emit(':added-definition', key, definition);
|
||||
}
|
||||
|
||||
|
||||
|
@ -612,8 +755,13 @@ export default class SettingsManager extends Module {
|
|||
if ( ! ui.key && ui.title )
|
||||
ui.key = ui.title.toSnakeCase();
|
||||
|
||||
const old_definition = this.ui_structures.get(key);
|
||||
this.ui_structures.set(key, definition);
|
||||
this.emit(':added-definition', key, definition);
|
||||
|
||||
// Do not re-emit `added-definition` when re-adding an existing
|
||||
// setting. Prevents the settings UI from goofing up.
|
||||
if ( ! old_definition )
|
||||
this.emit(':added-definition', key, definition);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -33,6 +33,10 @@ export class SettingsProvider extends EventEmitter {
|
|||
this.disabled = false;
|
||||
}
|
||||
|
||||
static supported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
@ -40,6 +44,11 @@ export class SettingsProvider extends EventEmitter {
|
|||
return Promise.reject(new Error('Not Implemented'));
|
||||
}
|
||||
|
||||
broadcastTransfer() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
disableEvents() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
|
||||
async flush() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, require-await
|
||||
|
||||
get(key, default_value) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
set(key, value) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
delete(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
|
@ -53,6 +62,13 @@ export class SettingsProvider extends EventEmitter {
|
|||
|
||||
get supportsBlobs() { return false; } // eslint-disable-line class-methods-use-this
|
||||
|
||||
async getBlob(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await
|
||||
async setBlob(key, value) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await
|
||||
async deleteBlob(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await
|
||||
async hasBlob(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await
|
||||
async clearBlobs() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await
|
||||
async blobKeys() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars, require-await
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -91,6 +107,46 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
broadcastTransfer() {
|
||||
this.broadcast({type: 'change-provider'});
|
||||
}
|
||||
|
||||
disableEvents() {
|
||||
if ( this._broadcaster ) {
|
||||
this._broadcaster.removeEventListener('message', this._boundHandleMessage);
|
||||
this._broadcaster.close();
|
||||
this._boundHandleMessage = this._broadcaster = null;
|
||||
}
|
||||
|
||||
if ( this._boundHandleStorage ) {
|
||||
window.removeEventListener('storage', this._boundHandleStorage);
|
||||
this._boundHandleStorage = null;
|
||||
}
|
||||
|
||||
this.broadcast = () => {};
|
||||
this.emit = () => {};
|
||||
}
|
||||
|
||||
|
||||
static key = 'local';
|
||||
static priority = -1000;
|
||||
static title = 'Local Storage';
|
||||
static description = '[Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) is available on all platforms and fast to access, but has poorly defined capacity limits and may be cleared unexpectedly. Particularly, clearing cookies and cache in your browser will likely clear Local Storage as well.';
|
||||
|
||||
// All environments that support FFZ support LocalStorage.
|
||||
static supported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static hasContent(prefix) {
|
||||
if ( ! prefix )
|
||||
prefix = 'FFZ:setting:';
|
||||
|
||||
for(const key in localStorage)
|
||||
if ( key.startsWith(prefix) && has(localStorage, key) )
|
||||
return true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
|
@ -112,6 +168,9 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
|
||||
flush() { /* no-op */ } // eslint-disable-line class-methods-use-this
|
||||
|
||||
|
||||
broadcast(msg) {
|
||||
if ( this._broadcaster )
|
||||
this._broadcaster.postMessage(msg);
|
||||
|
@ -125,7 +184,13 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
this.manager.log.debug('storage broadcast event', event.data);
|
||||
const {type, key} = event.data;
|
||||
|
||||
if ( type === 'set' ) {
|
||||
if ( type === 'change-provider') {
|
||||
this.manager.log.info('Received notice of changed settings provider.');
|
||||
this.emit('change-provider');
|
||||
this.disable();
|
||||
this.disableEvents();
|
||||
|
||||
} else if ( type === 'set' ) {
|
||||
const val = JSON.parse(localStorage.getItem(this.prefix + key));
|
||||
this._cached.set(key, val);
|
||||
this.emit('changed', key, val, false);
|
||||
|
@ -176,6 +241,9 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
set(key, value) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
if ( value === undefined ) {
|
||||
if ( this.has(key) )
|
||||
this.delete(key);
|
||||
|
@ -189,6 +257,9 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
delete(key) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
this._cached.delete(key);
|
||||
localStorage.removeItem(this.prefix + key);
|
||||
this.broadcast({type: 'delete', key});
|
||||
|
@ -204,6 +275,9 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
clear() {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const old_cache = this._cached;
|
||||
this._cached = new Map;
|
||||
|
||||
|
@ -226,32 +300,38 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
|
||||
|
||||
export class IndexedDBProvider extends SettingsProvider {
|
||||
constructor(manager) {
|
||||
constructor(manager, start = true) {
|
||||
super(manager);
|
||||
|
||||
this._start_time = performance.now();
|
||||
|
||||
this._pending = new Set;
|
||||
this._flush_wait = null;
|
||||
|
||||
this._cached = new Map;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
|
||||
if ( window.BroadcastChannel ) {
|
||||
const bc = this._broadcaster = new BroadcastChannel('ffz-settings');
|
||||
bc.addEventListener('message',
|
||||
this._boundHandleMessage = this.handleMessage.bind(this));
|
||||
if ( start ) {
|
||||
if ( window.BroadcastChannel ) {
|
||||
const bc = this._broadcaster = new BroadcastChannel('ffz-settings');
|
||||
bc.addEventListener('message',
|
||||
this._boundHandleMessage = this.handleMessage.bind(this));
|
||||
|
||||
} else {
|
||||
window.addEventListener('storage',
|
||||
this._boundHandleStorage = this.handleStorage.bind(this));
|
||||
} else {
|
||||
window.addEventListener('storage',
|
||||
this._boundHandleStorage = this.handleStorage.bind(this));
|
||||
}
|
||||
|
||||
this.loadSettings()
|
||||
.then(() => this._resolveReady(true))
|
||||
.catch(err => this._resolveReady(false, err));
|
||||
}
|
||||
|
||||
this.loadSettings()
|
||||
.then(() => this._resolveReady(true))
|
||||
.catch(err => this._resolveReady(false, err));
|
||||
}
|
||||
|
||||
_resolveReady(success, data) {
|
||||
this.manager.log.info(`IDB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
||||
if ( this.manager )
|
||||
this.manager.log.info(`IDB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
||||
this.ready = success;
|
||||
const waiters = this._ready_wait;
|
||||
this._ready_wait = null;
|
||||
|
@ -264,6 +344,46 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
return window.indexedDB != null;
|
||||
}
|
||||
|
||||
static hasContent() {
|
||||
return new Promise((s) => {
|
||||
const request = window.indexedDB.open('FFZ', DB_VERSION);
|
||||
request.onerror = () => s(false);
|
||||
|
||||
request.onupgradeneeded = e => {
|
||||
// TODO: Logic to detect that the version updated.
|
||||
// Can wait to implement till we actually increment version.
|
||||
e.target.transaction.abort();
|
||||
s(false);
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
|
||||
// We have a database, but does it contain anything?
|
||||
const trx = db.transaction(['settings'], 'readonly'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
const r2 = store.getAllKeys();
|
||||
|
||||
r2.onerror = () => {
|
||||
db.close();
|
||||
s(false);
|
||||
}
|
||||
|
||||
r2.onsuccess = () => {
|
||||
const success = Array.isArray(r2.result) && r2.result.length > 0;
|
||||
db.close();
|
||||
return s(success);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static key = 'idb';
|
||||
static priority = 10;
|
||||
static title = 'IndexedDB';
|
||||
static description = '[IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) is available on most platforms, and has a slightly slower initialization time than Local Storage. IndexedDB has a higher storage capacity and is less likely to be cleared unexpectedly.';
|
||||
|
||||
get supportsBlobs() { return true; } // eslint-disable-line class-methods-use-this
|
||||
|
||||
destroy() {
|
||||
|
@ -286,6 +406,59 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
broadcastTransfer() {
|
||||
this.broadcast({type: 'change-provider'});
|
||||
}
|
||||
|
||||
disableEvents() {
|
||||
if ( this._broadcaster ) {
|
||||
this._broadcaster.removeEventListener('message', this._boundHandleMessage);
|
||||
this._broadcaster.close();
|
||||
this._boundHandleMessage = this._broadcaster = null;
|
||||
}
|
||||
|
||||
this.broadcast = () => {};
|
||||
this.emit = () => {};
|
||||
}
|
||||
|
||||
|
||||
_onStart(obj) {
|
||||
if ( ! this._pending )
|
||||
this._pending = new Set;
|
||||
|
||||
this._pending.add(obj);
|
||||
}
|
||||
|
||||
_onFinish(obj) {
|
||||
if ( this._pending ) {
|
||||
this._pending.delete(obj);
|
||||
|
||||
if ( this._pending.size )
|
||||
return;
|
||||
}
|
||||
|
||||
if ( this._flush_wait ) {
|
||||
const waiters = this._flush_wait;
|
||||
this._flush_wait = null;
|
||||
|
||||
for(const waiter of waiters)
|
||||
waiter();
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
if ( ! this._pending || ! this._pending.size )
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise(s => {
|
||||
if ( ! this._flush_wait )
|
||||
this._flush_wait = [];
|
||||
|
||||
this._flush_wait.push(s);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
broadcast(msg) {
|
||||
if ( this._broadcaster )
|
||||
this._broadcaster.postMessage(msg);
|
||||
|
@ -296,14 +469,21 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
if ( this.disabled || ! event.isTrusted || ! event.data )
|
||||
return;
|
||||
|
||||
this.manager.log.debug('storage broadcast event', event.data);
|
||||
if ( this.manager )
|
||||
this.manager.log.debug('storage broadcast event', event.data);
|
||||
const {type, key} = event.data;
|
||||
|
||||
if ( type === 'set' ) {
|
||||
if ( type === 'change-provider') {
|
||||
this.manager.log.info('Received notice of changed settings provider.');
|
||||
this.emit('change-provider');
|
||||
this.disable();
|
||||
this.disableEvents();
|
||||
|
||||
} else if ( type === 'set' ) {
|
||||
this._get(key).then(val => {
|
||||
this._cached.set(key, val);
|
||||
this.emit('changed', key, val, false);
|
||||
}).catch(err => this.manager.log.error(`Error getting setting "${key}" from database`, err));
|
||||
}).catch(err => this.manager && this.manager.log.error(`Error getting setting "${key}" from database`, err));
|
||||
|
||||
} else if ( type === 'delete' ) {
|
||||
this._cached.delete(key);
|
||||
|
@ -314,6 +494,13 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
this._cached.clear();
|
||||
for(const key of old_keys)
|
||||
this.emit('changed', key, undefined, true);
|
||||
|
||||
} else if ( type === 'set-blob' ) {
|
||||
this.emit('changed-blob', key, false);
|
||||
} else if ( type === 'delete-blob' ) {
|
||||
this.emit('changed-blob', key, true);
|
||||
} else if ( type === 'clear-blobs' ) {
|
||||
this.emit('clear-blobs');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,6 +523,9 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
set(key, value) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
if ( value === undefined ) {
|
||||
if ( this.has(key) )
|
||||
this.delete(key);
|
||||
|
@ -345,15 +535,18 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
this._cached.set(key, value);
|
||||
this._set(key, value)
|
||||
.then(() => this.broadcast({type: 'set', key}))
|
||||
.catch(err => this.manager.log.error(`Error saving setting "${key}" to database`, err));
|
||||
.catch(err => this.manager && this.manager.log.error(`Error saving setting "${key}" to database`, err));
|
||||
|
||||
this.emit('set', key, value, false);
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
this._cached.delete(key);
|
||||
this._delete(key)
|
||||
.catch(err => this.manager.log.error(`Error deleting setting "${key}" from database`, err))
|
||||
.catch(err => this.manager && this.manager.log.error(`Error deleting setting "${key}" from database`, err))
|
||||
.then(() => this.broadcast({type: 'delete', key}));
|
||||
|
||||
this.emit('set', key, undefined, true);
|
||||
|
@ -368,6 +561,9 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
clear() {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const old_cache = this._cached;
|
||||
this._cached = new Map;
|
||||
|
||||
|
@ -375,7 +571,7 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
this.emit('changed', key, undefined, true);
|
||||
|
||||
this._clear()
|
||||
.catch(err => this.manager.log.error(`Error clearing database`, err))
|
||||
.catch(err => this.manager && this.manager.log.error(`Error clearing database`, err))
|
||||
.then(() => this.broadcast({type: 'clear'}));
|
||||
}
|
||||
|
||||
|
@ -408,13 +604,18 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
const request = window.indexedDB.open('FFZ', DB_VERSION);
|
||||
this._onStart(request);
|
||||
|
||||
request.onerror = e => {
|
||||
this.manager.log.error('Error opening database.', e);
|
||||
if ( this.manager )
|
||||
this.manager.log.error('Error opening database.', e);
|
||||
done(false, e);
|
||||
this._onFinish(request);
|
||||
}
|
||||
|
||||
request.onupgradeneeded = e => {
|
||||
this.manager.log.info(`Upgrading database from version ${e.oldVersion} to ${DB_VERSION}`);
|
||||
if ( this.manager )
|
||||
this.manager.log.info(`Upgrading database from version ${e.oldVersion} to ${DB_VERSION}`);
|
||||
|
||||
const db = request.result;
|
||||
|
||||
|
@ -423,9 +624,11 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.manager.log.info(`Database opened. (After: ${(performance.now() - this._start_time).toFixed(5)}ms)`);
|
||||
if ( this.manager )
|
||||
this.manager.log.info(`Database opened. (After: ${(performance.now() - this._start_time).toFixed(5)}ms)`);
|
||||
this.db = request.result;
|
||||
done(true, this.db);
|
||||
this._onFinish(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -438,39 +641,45 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
|
||||
return new Promise((s,f) => {
|
||||
const request = store.getAll();
|
||||
this._onStart(request);
|
||||
|
||||
request.onsuccess = () => {
|
||||
for(const entry of request.result)
|
||||
this._cached.set(entry.k, entry.v);
|
||||
|
||||
s();
|
||||
this._onFinish(request);
|
||||
}
|
||||
|
||||
request.onerror = err => {
|
||||
this.manager.log.error('Error reading settings from database.', err);
|
||||
if ( this.manager )
|
||||
this.manager.log.error('Error reading settings from database.', err);
|
||||
f();
|
||||
this._onFinish(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*cursor = store.openCursor();
|
||||
|
||||
async _getKeys() {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readonly'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
return new Promise((s,f) => {
|
||||
cursor.onsuccess = e => {
|
||||
const entry = e.target.result;
|
||||
if ( entry ) {
|
||||
this._cached.set(entry.key, entry.value);
|
||||
entry.continue();
|
||||
} else {
|
||||
// We're done~!
|
||||
s();
|
||||
}
|
||||
};
|
||||
const request = store.getAllKeys();
|
||||
this._onStart(request);
|
||||
|
||||
cursor.onerror = e => {
|
||||
this.manager.log.error('Error reading settings from database.', e);
|
||||
f(e);
|
||||
request.onsuccess = () => {
|
||||
s(request.result);
|
||||
this._onFinish(request);
|
||||
}
|
||||
});*/
|
||||
|
||||
request.onerror = () => {
|
||||
f();
|
||||
this._onFinish(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -483,15 +692,25 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
store.onerror = f;
|
||||
|
||||
const req = store.get(key);
|
||||
req.onerror = f;
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
|
||||
req.onsuccess = () => {
|
||||
s(req.result.v);
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async _set(key, value) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings');
|
||||
|
@ -500,13 +719,24 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
store.onerror = f;
|
||||
|
||||
const req = store.put({k: key, v: value});
|
||||
req.onerror = f;
|
||||
req.onsuccess = s;
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
s();
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async _delete(key) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings');
|
||||
|
@ -515,13 +745,24 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
store.onerror = f;
|
||||
|
||||
const req = store.delete(key);
|
||||
req.onerror = f;
|
||||
req.onsuccess = s;
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
s();
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async _clear() {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings');
|
||||
|
@ -530,11 +771,151 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
store.onerror = f;
|
||||
|
||||
const req = store.clear();
|
||||
req.onerror = f;
|
||||
req.onsuccess = s;
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
s();
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Blobs */
|
||||
|
||||
async getBlob(key) {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['blobs'], 'readonly'),
|
||||
store = trx.objectStore('blobs');
|
||||
|
||||
return new Promise((s, f) => {
|
||||
store.onerror = f;
|
||||
|
||||
const req = store.get(key);
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = e => {
|
||||
s(e.target.result);
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setBlob(key, value) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['blobs'], 'readwrite'),
|
||||
store = trx.objectStore('blobs');
|
||||
|
||||
return new Promise((s, f) => {
|
||||
store.onerror = f;
|
||||
|
||||
const req = store.put(value, key);
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
s();
|
||||
|
||||
this.broadcast({type: 'set-blob', key});
|
||||
this.emit('set-blob', key, value, false);
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBlob(key) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['blobs'], 'readwrite'),
|
||||
store = trx.objectStore('blobs');
|
||||
|
||||
return new Promise((s, f) => {
|
||||
store.onerror = f;
|
||||
|
||||
const req = store.delete(key);
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
s();
|
||||
this.broadcast({type: 'delete-blob', key});
|
||||
this.emit('set-blob', key, undefined, true);
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async hasBlob(key) {
|
||||
const keys = await this.blobKeys();
|
||||
return keys.includes(key);
|
||||
}
|
||||
|
||||
async clearBlobs() {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['blobs'], 'readwrite'),
|
||||
store = trx.objectStore('blobs');
|
||||
|
||||
return new Promise((s, f) => {
|
||||
store.onerror = f;
|
||||
|
||||
const req = store.clear();
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
s();
|
||||
this.broadcast({type: 'clear-blobs'});
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async blobKeys() {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['blobs'], 'readonly'),
|
||||
store = trx.objectStore('blobs');
|
||||
|
||||
return new Promise((s, f) => {
|
||||
const req = store.getAllKeys();
|
||||
this._onStart(req);
|
||||
|
||||
req.onerror = () => {
|
||||
f();
|
||||
this._onFinish(req);
|
||||
}
|
||||
req.onsuccess = () => {
|
||||
if ( Array.isArray(req.result) )
|
||||
s(req.result);
|
||||
else
|
||||
f();
|
||||
|
||||
this._onFinish(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -232,6 +232,13 @@ export default class MenuButton extends SiteModule {
|
|||
this.on('i18n:changed-strings', this.update);
|
||||
this.on('i18n:update', this.update);
|
||||
this.on('addons:data-loaded', this.update);
|
||||
this.on('settings:change-provider', () => {
|
||||
this.error = {
|
||||
i18n: 'site.menu_button.changed',
|
||||
text: 'The FrankerFaceZ settings provider has changed. Please refresh this tab to avoid strange behavior.'
|
||||
};
|
||||
this.update()
|
||||
});
|
||||
}
|
||||
|
||||
updateButton(inst) {
|
||||
|
@ -313,14 +320,14 @@ export default class MenuButton extends SiteModule {
|
|||
<div class="tw-flex-grow-1">
|
||||
{ this.error.i18n ? this.i18n.t(this.error.i18n, this.error.text) : this.error.text }
|
||||
</div>
|
||||
<button
|
||||
{this.error.permanent ? null : (<button
|
||||
class="tw-button-icon tw-mg-l-05 tw-relative tw-tooltip__container"
|
||||
onClick={() => this.error = null} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-cancel" />
|
||||
</span>
|
||||
</button>
|
||||
</button>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
|
|
|
@ -27,6 +27,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ffz-radio-top {
|
||||
.tw-radio__input:checked+.tw-radio__label:after,
|
||||
.tw-radio__label:before {
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ffz--i18n-entry {
|
||||
&:nth-child(2n+1) {
|
||||
background-color: var(--color-background-alt-2);
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
|
||||
& > div {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
& > .ffz--menu-container {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
& > header {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue