mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-25 08:01:20 -08:00
refactor(core): CORE-14 split typed settings and scope tree lifetimes
This commit is contained in:
1 parent
697817eceb
commit
64d1939daa
51 files changed
+28144
-1519
No files matched your search
@@ -4,6 +4,91 @@ ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hoo
|
||||
The production entry is still `src/index.js`; this document marks actual migrated
|
||||
boundaries rather than describing the entire core as TypeScript.
|
||||
|
||||
## Settings (CORE-14)
|
||||
|
||||
setting/index.ts remains the specialized manager: panel cache keys are option arrays,
|
||||
add/update return the original item, remove returns undefined and missing find returns
|
||||
null. A narrow constructor type view preserves the actual Component prototype chain
|
||||
without imposing its incompatible string-keyed cache or registry return types.
|
||||
It delegates tree work to setting/model.ts and measurement to setting/layout.ts.
|
||||
The tree model preserves object/array identity and immutable hidden getter descriptors,
|
||||
while rebinding a removed/moved node to its current parent. Automatic names skip explicit
|
||||
names anywhere in the tree. Structural validation runs before installing bindings;
|
||||
traverse retains preorder and callback-driven child insertion. Model ownership uses
|
||||
an opaque owner state and root scope, with no direct player/manager reference. Closing
|
||||
the owner clears its root and scope references so bindings do not retain the old owner.
|
||||
It rejects an item that still belongs to another active tree before modifying either
|
||||
tree. Removing the subtree or closing its owner permits original-object reuse, with
|
||||
the same immutable getters and event array. Simultaneous sharing was already broken
|
||||
in the published player (the second instance had no row); it now reports an explicit
|
||||
ownership error instead of allowing the new player to steal the first player's row.
|
||||
|
||||
The layout module computes dimensions independently from DOM reads. Width is bounded
|
||||
by player width and resolved bottom padding; height fits above actual control rows,
|
||||
using the existing scrolling panel. Control coordinates account for CSS scaling.
|
||||
The existing right-edge CSS fallback and mobile/rotation positioning branches remain.
|
||||
render.ts now owns DOM/descriptor rendering, selection.ts owns callback generations,
|
||||
and resources.ts owns item listeners, deferred mounts and recursive panel disposal.
|
||||
The four builtin setting factories are TS and release their Emitter subscriptions.
|
||||
events.ts owns root subscriptions and size/style observers. Control height changes
|
||||
can animate the panel's bottom offset; transitionend recalculates its final height.
|
||||
The MutationObserver fallback also consumes the existing resize event for container
|
||||
changes. All these callbacks stop when the instance closes.
|
||||
|
||||
Failed additions unlink the attempted entry without releasing a duplicate's existing
|
||||
listeners. Successfully formatted new entries also release partial render resources.
|
||||
registration.ts prevents an obsolete failed add from removing a reentrant successful
|
||||
add/update of the same object; removal and update supersede the pending registration.
|
||||
Removal finishes row cleanup and root rendering even if event removal throws, then
|
||||
reports collected errors. Nonextensible metadata and unwritable generated names are
|
||||
checked before binding earlier entries. These checks do not roll back arbitrary
|
||||
user getter/setter or Proxy side effects.
|
||||
|
||||
panels.ts gives each cached panel a root-owned scope. Rows are child scopes and the
|
||||
back header is owned by its panel while retaining the parent's public event array.
|
||||
Disposing the panel removes its cache entry, listeners, pending mounts and DOM.
|
||||
New-panel rendering checkpoints item descriptors and the original position of raw
|
||||
content nodes, without calling user getters. A failure restores those inputs, the
|
||||
previous active panel and owned layout properties. Creation stops if a header hook
|
||||
removes the parent or destroys the instance.
|
||||
|
||||
update.ts suspends the old subtree instead of disposing it before assignment. The
|
||||
same public event array temporarily holds only replacement registrations; suspended
|
||||
scopes retain their old registrations privately. activity.ts prevents suspended
|
||||
callbacks and asynchronous writes. Success disposes the old resources; failure
|
||||
resumes the original registrations and restores descriptors, real DOM nodes, range
|
||||
array identity/live input values, switch state, tree bindings and owned layout.
|
||||
The current input getter order is retained, but assignment stops after a reentrant
|
||||
operation supersedes it. Pending operations are tracked across their descendants;
|
||||
removing an ancestor cancels its child's update before releasing the subtree.
|
||||
|
||||
Renderers retain their captured operation/scope so a late getter failure cannot
|
||||
dispose a newer row. Cleanup failures after a committed replacement are reported;
|
||||
the successful new row is not rolled back to already-disposed resources. Arbitrary
|
||||
user accessor/Proxy side effects remain outside the managed-state rollback boundary.
|
||||
|
||||
Cached-panel navigation now restores the previous panel/layout on failure. A newer
|
||||
navigation, removal or destruction supersedes that restoration, even if the failing
|
||||
outer call resumes afterward. It does not rebuild cached rows or their listeners.
|
||||
|
||||
Long labels and tooltips shrink with ellipsis while retaining their full DOM content.
|
||||
Icons and native controls keep their width, including in a 320px player. Tests cover
|
||||
trusted keyboard input/change ordering, pointer switch callbacks, touch selection,
|
||||
and actual builtin autoOrientation transforms with a local video. Mobile UA/viewport
|
||||
and touch emulation are not certification on physical mobile devices.
|
||||
|
||||
Public Setting return declaration discrepancies remain tracked by BASE-TYPE-07 for
|
||||
the compatible public facade at CORE-21. The source manager has precise item/null/void
|
||||
returns and does not change runtime behavior to match incorrect legacy declarations.
|
||||
|
||||
Tests: setting-model.test.js, setting-layout.test.js, setting-resources.test.js,
|
||||
types/setting-model.ts, types/setting-manager.ts and
|
||||
browser/setting.spec.js, browser/setting-update.spec.js and browser/setting-ownership.spec.js.
|
||||
browser/setting-interaction.spec.js covers real input, long labels and mobile rotation.
|
||||
Run the shared test scripts and the full installed UMD/legacy browser matrices after
|
||||
changing these boundaries. The per-package and final release tasks own the complete
|
||||
ecosystem/SDK matrix, remote CI and physical-device release review.
|
||||
|
||||
## Components and controls (CORE-13)
|
||||
|
||||
The public registries remain Component-based. utils/component.ts owns add/update/remove,
|
||||
|
||||
Vendored
+1
-1
@@ -67,4 +67,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),r.exports);const n=t(o),a="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),r.exports);const n=t(o),a="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
+1
-1
@@ -67,4 +67,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,h={exports:{}};var f=(u||(u=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),h.exports);const m=p(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var a=t=>{try{s(r.next(t))}catch(e){o(e)}},i=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,h={exports:{}};var f=(u||(u=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,n),i(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),a(s,l,c),i(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),a(l,c,d),i(l,c,d),o(l,c,d)})}}(t,e,n),t}function a(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function i(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var a=r(o);throw"string"===a?new Error(o):"error"===a?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=p(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+1221
-454
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
|
||||
const pauses = new WeakMap<ResourceScope, number>()
|
||||
|
||||
export function settingScopeActive(scope: ResourceScope): boolean {
|
||||
return !scope.closed && !pauses.get(scope)
|
||||
}
|
||||
|
||||
export function pauseSettingScope(scope: ResourceScope): () => void {
|
||||
pauses.set(scope, (pauses.get(scope) || 0) + 1)
|
||||
let paused = true
|
||||
return () => {
|
||||
if (!paused)
|
||||
return
|
||||
paused = false
|
||||
const count = (pauses.get(scope) || 1) - 1
|
||||
if (count)
|
||||
pauses.set(scope, count)
|
||||
else
|
||||
pauses.delete(scope)
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -1,11 +1,14 @@
|
||||
export default function aspectRatio(art) {
|
||||
import type { SettingHost, SettingItem } from './types'
|
||||
import { subscribeSetting } from './resources'
|
||||
|
||||
export default function aspectRatio(art: SettingHost): SettingItem {
|
||||
const {
|
||||
i18n,
|
||||
icons,
|
||||
constructor: { SETTING_ITEM_WIDTH, ASPECT_RATIO },
|
||||
} = art
|
||||
|
||||
function getI18n(value) {
|
||||
function getI18n(value: string) {
|
||||
return value === 'default' ? i18n.get('Default') : value
|
||||
}
|
||||
|
||||
@@ -29,12 +32,12 @@ export default function aspectRatio(art) {
|
||||
}
|
||||
}),
|
||||
onSelect(item) {
|
||||
art.aspectRatio = item.value
|
||||
art.aspectRatio = item.value as string
|
||||
return item.html
|
||||
},
|
||||
mounted: () => {
|
||||
mounted: (_element, item) => {
|
||||
update()
|
||||
art.on('aspectRatio', () => update())
|
||||
subscribeSetting(art, item, 'aspectRatio', () => update())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { SettingItem } from './types'
|
||||
|
||||
type CaptureState = () => () => void
|
||||
const states = new WeakMap<SettingItem, CaptureState>()
|
||||
|
||||
export function rememberSettingState(item: SettingItem, capture?: CaptureState): void {
|
||||
if (capture)
|
||||
states.set(item, capture)
|
||||
else
|
||||
states.delete(item)
|
||||
}
|
||||
|
||||
// Capture descriptors without invoking user getters while preparing a render.
|
||||
export function captureSettingItem(item: SettingItem): () => void {
|
||||
const descriptors = new Map(Reflect.ownKeys(item).map(key => [key, Object.getOwnPropertyDescriptor(item, key)!]))
|
||||
const nodes: { node: Node, parent: Node | null, next: Node | null }[] = []
|
||||
const state = states.get(item)
|
||||
const restoreState = state?.()
|
||||
for (const key of ['html', 'icon', 'tooltip']) {
|
||||
const value: unknown = descriptors.get(key)?.value
|
||||
if (value instanceof Node)
|
||||
nodes.push({ node: value, parent: value.parentNode, next: value.nextSibling })
|
||||
}
|
||||
return () => {
|
||||
for (const key of Reflect.ownKeys(item)) {
|
||||
if (!descriptors.has(key) && Object.getOwnPropertyDescriptor(item, key)?.configurable)
|
||||
Reflect.deleteProperty(item, key)
|
||||
}
|
||||
for (const [key, descriptor] of descriptors)
|
||||
Object.defineProperty(item, key, descriptor)
|
||||
restoreState?.()
|
||||
rememberSettingState(item, state)
|
||||
for (const { node, parent, next } of nodes) {
|
||||
if (parent)
|
||||
parent.insertBefore(node, next?.parentNode === parent ? next : null)
|
||||
else
|
||||
node.parentNode?.removeChild(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { SettingEvents, SettingManager } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { includeFromEvent } from '../utils/dom'
|
||||
|
||||
export function installSettingEvents(setting: SettingManager): void {
|
||||
const { art } = setting
|
||||
const scope = getScope(art).child()
|
||||
const on = <Name extends keyof SettingEvents>(name: Name, callback: (...args: SettingEvents[Name]) => unknown) => {
|
||||
if (scope.closed || isClosing(art))
|
||||
return
|
||||
const guarded = (...args: SettingEvents[Name]) => {
|
||||
if (!scope.closed && !isClosing(art))
|
||||
return callback(...args)
|
||||
}
|
||||
art.on(name, guarded)
|
||||
scope.add(() => {
|
||||
art.off(name, guarded)
|
||||
})
|
||||
}
|
||||
on('blur', () => {
|
||||
if (setting.show) {
|
||||
setting.show = false
|
||||
setting.render()
|
||||
}
|
||||
})
|
||||
on('focus', (event) => {
|
||||
const isControl = includeFromEvent(event, art.controls.setting)
|
||||
const isSetting = includeFromEvent(event, setting.$parent)
|
||||
if (setting.show && !isControl && !isSetting) {
|
||||
setting.show = false
|
||||
setting.render()
|
||||
}
|
||||
})
|
||||
on('resize', () => setting.resize())
|
||||
const resize = () => {
|
||||
if (!scope.closed && !isClosing(art))
|
||||
setting.resize()
|
||||
}
|
||||
const transitioned = art.proxy(setting.$parent, 'transitionend', (event) => {
|
||||
if (event.target === setting.$parent && (event as TransitionEvent).propertyName === 'bottom')
|
||||
resize()
|
||||
})
|
||||
scope.add(() => {
|
||||
art.events.remove(transitioned)
|
||||
})
|
||||
const { $player, $controls } = art.template
|
||||
if (!scope.closed && typeof ResizeObserver !== 'undefined') {
|
||||
const observer = new ResizeObserver(resize)
|
||||
observer.observe($player)
|
||||
observer.observe($controls)
|
||||
scope.add(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
if (!scope.closed && typeof MutationObserver !== 'undefined') {
|
||||
const observer = new MutationObserver(resize)
|
||||
if (typeof ResizeObserver === 'undefined')
|
||||
observer.observe($controls, { childList: true, subtree: true, attributes: true, characterData: true })
|
||||
// Controls can publish their measured height after our ResizeObserver callback.
|
||||
observer.observe($player, { attributes: true, attributeFilter: ['class', 'style'] })
|
||||
scope.add(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { SettingHost, SettingItem } from './types'
|
||||
import { capitalize } from '../utils'
|
||||
import { subscribeSetting } from './resources'
|
||||
|
||||
export default function flip(art) {
|
||||
export default function flip(art: SettingHost): SettingItem {
|
||||
const {
|
||||
i18n,
|
||||
icons,
|
||||
constructor: { SETTING_ITEM_WIDTH, FLIP },
|
||||
} = art
|
||||
|
||||
function getI18n(value) {
|
||||
function getI18n(value: string) {
|
||||
return i18n.get(capitalize(value))
|
||||
}
|
||||
|
||||
@@ -31,12 +33,12 @@ export default function flip(art) {
|
||||
}
|
||||
}),
|
||||
onSelect(item) {
|
||||
art.flip = item.value
|
||||
art.flip = item.value as string
|
||||
return item.html
|
||||
},
|
||||
mounted: () => {
|
||||
mounted: (_element, item) => {
|
||||
update()
|
||||
art.on('flip', () => update())
|
||||
subscribeSetting(art, item, 'flip', () => update())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import {
|
||||
addClass,
|
||||
append,
|
||||
createElement,
|
||||
def,
|
||||
errorHandle,
|
||||
getRect,
|
||||
has,
|
||||
includeFromEvent,
|
||||
inverseClass,
|
||||
isMobile,
|
||||
remove,
|
||||
replaceElement,
|
||||
setStyle,
|
||||
} from '../utils'
|
||||
import Component from '../utils/component'
|
||||
import aspectRatio from './aspectRatio'
|
||||
import flip from './flip'
|
||||
import playbackRate from './playbackRate'
|
||||
import subtitleOffset from './subtitleOffset'
|
||||
|
||||
export default class Setting extends Component {
|
||||
constructor(art) {
|
||||
super(art)
|
||||
|
||||
const {
|
||||
option,
|
||||
controls,
|
||||
template: { $setting },
|
||||
} = art
|
||||
|
||||
this.name = 'setting'
|
||||
this.$parent = $setting
|
||||
|
||||
this.id = 0
|
||||
this.active = null
|
||||
this.cache = new Map()
|
||||
this.option = [...this.builtin, ...option.settings]
|
||||
|
||||
if (option.setting) {
|
||||
this.format()
|
||||
this.render()
|
||||
|
||||
art.on('blur', () => {
|
||||
if (this.show) {
|
||||
this.show = false
|
||||
this.render()
|
||||
}
|
||||
})
|
||||
|
||||
art.on('focus', (event) => {
|
||||
const isControl = includeFromEvent(event, controls.setting)
|
||||
const isSetting = includeFromEvent(event, this.$parent)
|
||||
if (this.show && !isControl && !isSetting) {
|
||||
this.show = false
|
||||
this.render()
|
||||
}
|
||||
})
|
||||
|
||||
art.on('resize', () => this.resize())
|
||||
}
|
||||
}
|
||||
|
||||
get builtin() {
|
||||
const result = []
|
||||
const { option } = this.art
|
||||
|
||||
if (option.playbackRate) {
|
||||
result.push(playbackRate(this.art))
|
||||
}
|
||||
|
||||
if (option.aspectRatio) {
|
||||
result.push(aspectRatio(this.art))
|
||||
}
|
||||
|
||||
if (option.flip) {
|
||||
result.push(flip(this.art))
|
||||
}
|
||||
|
||||
if (option.subtitleOffset) {
|
||||
result.push(subtitleOffset(this.art))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
traverse(callback, option = this.option) {
|
||||
for (let index = 0; index < option.length; index++) {
|
||||
const item = option[index]
|
||||
callback(item)
|
||||
if (item.selector?.length) {
|
||||
this.traverse(callback, item.selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check(target) {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
target.$parent.tooltip = target.html
|
||||
this.traverse((item) => {
|
||||
item.default = item === target
|
||||
if (item.default && item.$item) {
|
||||
inverseClass(item.$item, 'art-current')
|
||||
}
|
||||
}, target.$option)
|
||||
this.render(target.$parents)
|
||||
}
|
||||
|
||||
format(option = this.option, parent, parents, names = []) {
|
||||
for (let index = 0; index < option.length; index++) {
|
||||
const item = option[index]
|
||||
|
||||
if (item?.name) {
|
||||
errorHandle(!names.includes(item.name), `The [${item.name}] already exists in [setting]`)
|
||||
names.push(item.name)
|
||||
}
|
||||
else {
|
||||
item.name = `setting-${this.id++}`
|
||||
}
|
||||
|
||||
if (!item.$formatted) {
|
||||
def(item, '$parent', {
|
||||
get: () => parent,
|
||||
})
|
||||
|
||||
def(item, '$parents', {
|
||||
get: () => parents,
|
||||
})
|
||||
|
||||
def(item, '$option', {
|
||||
get: () => option,
|
||||
})
|
||||
|
||||
const $events = []
|
||||
def(item, '$events', {
|
||||
get: () => $events,
|
||||
})
|
||||
|
||||
def(item, '$formatted', {
|
||||
get: () => true,
|
||||
})
|
||||
}
|
||||
|
||||
this.format(item.selector || [], item, option, names)
|
||||
}
|
||||
|
||||
this.option = option
|
||||
}
|
||||
|
||||
find(name = '') {
|
||||
let result = null
|
||||
this.traverse((item) => {
|
||||
if (item.name === name) {
|
||||
result = item
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
resize() {
|
||||
const {
|
||||
controls,
|
||||
constructor: { SETTING_WIDTH, SETTING_ITEM_HEIGHT },
|
||||
template: { $player, $setting },
|
||||
} = this.art
|
||||
|
||||
if (controls.setting && this.show) {
|
||||
const settingWidth = this.active[0]?.$parent?.width || SETTING_WIDTH
|
||||
const { left: controlLeft, width: controlWidth } = getRect(controls.setting)
|
||||
const { left: playerLeft, width: playerWidth } = getRect($player)
|
||||
const settingLeft = controlLeft - playerLeft + controlWidth / 2 - settingWidth / 2
|
||||
|
||||
const settingHeight
|
||||
= this.active === this.option
|
||||
? this.active.length * SETTING_ITEM_HEIGHT
|
||||
: (this.active.length + 1) * SETTING_ITEM_HEIGHT
|
||||
|
||||
setStyle($setting, 'height', `${settingHeight}px`)
|
||||
setStyle($setting, 'width', `${settingWidth}px`)
|
||||
|
||||
if (this.art.isRotate || isMobile)
|
||||
return
|
||||
|
||||
if (settingLeft + settingWidth > playerWidth) {
|
||||
setStyle($setting, 'left', null)
|
||||
setStyle($setting, 'right', null)
|
||||
}
|
||||
else {
|
||||
setStyle($setting, 'left', `${settingLeft}px`)
|
||||
setStyle($setting, 'right', 'auto')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inactivate(item) {
|
||||
for (let index = 0; index < item.$events.length; index++) {
|
||||
this.art.events.remove(item.$events[index])
|
||||
}
|
||||
item.$events.length = 0
|
||||
}
|
||||
|
||||
remove(name) {
|
||||
const item = this.find(name)
|
||||
errorHandle(item, `Can't find [${name}] in the [setting]`)
|
||||
const index = item.$option.indexOf(item)
|
||||
item.$option.splice(index, 1)
|
||||
this.inactivate(item)
|
||||
if (item.$item)
|
||||
remove(item.$item)
|
||||
this.render()
|
||||
}
|
||||
|
||||
update(target) {
|
||||
const item = this.find(target.name)
|
||||
|
||||
if (item) {
|
||||
this.inactivate(item)
|
||||
Object.assign(item, target)
|
||||
this.format()
|
||||
this.createItem(item, true)
|
||||
this.render()
|
||||
return item
|
||||
}
|
||||
else {
|
||||
return this.add(target)
|
||||
}
|
||||
}
|
||||
|
||||
add(item, option = this.option) {
|
||||
option.push(item)
|
||||
this.format()
|
||||
this.createItem(item)
|
||||
this.render()
|
||||
return item
|
||||
}
|
||||
|
||||
createHeader(item) {
|
||||
if (!this.cache.has(item.$option))
|
||||
return
|
||||
const $panel = this.cache.get(item.$option)
|
||||
|
||||
const {
|
||||
proxy,
|
||||
icons: { arrowLeft },
|
||||
constructor: { SETTING_ITEM_HEIGHT },
|
||||
} = this.art
|
||||
|
||||
const $item = createElement('div')
|
||||
setStyle($item, 'height', `${SETTING_ITEM_HEIGHT}px`)
|
||||
addClass($item, 'art-setting-item')
|
||||
addClass($item, 'art-setting-item-back')
|
||||
const $left = append($item, '<div class="art-setting-item-left"></div>')
|
||||
const $icon = createElement('div')
|
||||
addClass($icon, 'art-setting-item-left-icon')
|
||||
append($icon, arrowLeft)
|
||||
append($left, $icon)
|
||||
append($left, item.$parent.html)
|
||||
const event = proxy($item, 'click', () => this.render(item.$parents))
|
||||
item.$parent.$events.push(event)
|
||||
append($panel, $item)
|
||||
}
|
||||
|
||||
createItem(item, isUpdate = false) {
|
||||
if (!this.cache.has(item.$option))
|
||||
return
|
||||
const $panel = this.cache.get(item.$option)
|
||||
const oldItem = item.$item
|
||||
|
||||
let type = 'selector'
|
||||
|
||||
if (has(item, 'switch')) {
|
||||
type = 'switch'
|
||||
}
|
||||
|
||||
if (has(item, 'range')) {
|
||||
type = 'range'
|
||||
}
|
||||
|
||||
if (has(item, 'onClick')) {
|
||||
type = 'button'
|
||||
}
|
||||
|
||||
const { icons, proxy, constructor } = this.art
|
||||
|
||||
const $item = createElement('div')
|
||||
addClass($item, 'art-setting-item')
|
||||
setStyle($item, 'height', `${constructor.SETTING_ITEM_HEIGHT}px`)
|
||||
|
||||
$item.dataset.name = item.name || ''
|
||||
$item.dataset.value = item.value || ''
|
||||
|
||||
const $left = append($item, '<div class="art-setting-item-left"></div>')
|
||||
const $right = append($item, '<div class="art-setting-item-right"></div>')
|
||||
|
||||
const $icon = createElement('div')
|
||||
addClass($icon, 'art-setting-item-left-icon')
|
||||
|
||||
switch (type) {
|
||||
case 'button':
|
||||
case 'switch':
|
||||
case 'range':
|
||||
append($icon, item.icon || icons.config)
|
||||
break
|
||||
case 'selector':
|
||||
if (item.selector?.length) {
|
||||
append($icon, item.icon || icons.config)
|
||||
}
|
||||
else {
|
||||
append($icon, icons.check)
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
append($left, $icon)
|
||||
|
||||
def(item, '$icon', {
|
||||
configurable: true,
|
||||
get: () => $icon,
|
||||
})
|
||||
|
||||
def(item, 'icon', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return $icon.innerHTML
|
||||
},
|
||||
set(value) {
|
||||
$icon.innerHTML = ''
|
||||
append($icon, value)
|
||||
},
|
||||
})
|
||||
|
||||
const $html = createElement('div')
|
||||
addClass($html, 'art-setting-item-left-text')
|
||||
append($html, item.html || '')
|
||||
append($left, $html)
|
||||
|
||||
def(item, '$html', {
|
||||
configurable: true,
|
||||
get: () => $html,
|
||||
})
|
||||
|
||||
def(item, 'html', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return $html.innerHTML
|
||||
},
|
||||
set(value) {
|
||||
$html.innerHTML = ''
|
||||
append($html, value)
|
||||
},
|
||||
})
|
||||
|
||||
const $tooltip = createElement('div')
|
||||
addClass($tooltip, 'art-setting-item-right-tooltip')
|
||||
append($tooltip, item.tooltip || '')
|
||||
append($right, $tooltip)
|
||||
|
||||
def(item, '$tooltip', {
|
||||
configurable: true,
|
||||
get: () => $tooltip,
|
||||
})
|
||||
|
||||
def(item, 'tooltip', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return $tooltip.innerHTML
|
||||
},
|
||||
set(value) {
|
||||
$tooltip.innerHTML = ''
|
||||
append($tooltip, value)
|
||||
},
|
||||
})
|
||||
|
||||
switch (type) {
|
||||
case 'switch': {
|
||||
const $switch = createElement('div')
|
||||
addClass($switch, 'art-setting-item-right-icon')
|
||||
const $switchOn = append($switch, icons.switchOn)
|
||||
const $switchOff = append($switch, icons.switchOff)
|
||||
setStyle(item.switch ? $switchOff : $switchOn, 'display', 'none')
|
||||
append($right, $switch)
|
||||
|
||||
def(item, '$switch', {
|
||||
configurable: true,
|
||||
get: () => $switch,
|
||||
})
|
||||
|
||||
let $switchValue = item.switch
|
||||
def(item, 'switch', {
|
||||
configurable: true,
|
||||
get: () => $switchValue,
|
||||
set(value) {
|
||||
$switchValue = value
|
||||
if (value) {
|
||||
setStyle($switchOff, 'display', 'none')
|
||||
setStyle($switchOn, 'display', null)
|
||||
}
|
||||
else {
|
||||
setStyle($switchOff, 'display', null)
|
||||
setStyle($switchOn, 'display', 'none')
|
||||
}
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'range':
|
||||
{
|
||||
const $state = createElement('div')
|
||||
addClass($state, 'art-setting-item-right-icon')
|
||||
const $range = append($state, '<input type="range">')
|
||||
$range.value = item.range[0]
|
||||
$range.min = item.range[1]
|
||||
$range.max = item.range[2]
|
||||
$range.step = item.range[3]
|
||||
addClass($range, 'art-setting-range')
|
||||
append($right, $state)
|
||||
|
||||
def(item, '$range', {
|
||||
configurable: true,
|
||||
get: () => $range,
|
||||
})
|
||||
|
||||
let $rangeValue = [...item.range]
|
||||
def(item, 'range', {
|
||||
configurable: true,
|
||||
get: () => $rangeValue,
|
||||
set(value) {
|
||||
$rangeValue = [...value]
|
||||
$range.value = value[0]
|
||||
$range.min = value[1]
|
||||
$range.max = value[2]
|
||||
$range.step = value[3]
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'selector':
|
||||
if (item.selector?.length) {
|
||||
const $state = createElement('div')
|
||||
addClass($state, 'art-setting-item-right-icon')
|
||||
append($state, icons.arrowRight)
|
||||
append($right, $state)
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'switch': {
|
||||
if (item.onSwitch) {
|
||||
const event = proxy($item, 'click', async (event) => {
|
||||
item.switch = await item.onSwitch.call(this.art, item, $item, event)
|
||||
})
|
||||
item.$events.push(event)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'range': {
|
||||
if (item.$range) {
|
||||
if (item.onRange) {
|
||||
const event = proxy(item.$range, 'change', async (event) => {
|
||||
item.range[0] = item.$range.valueAsNumber
|
||||
item.tooltip = await item.onRange.call(this.art, item, $item, event)
|
||||
})
|
||||
item.$events.push(event)
|
||||
}
|
||||
|
||||
if (item.onChange) {
|
||||
const event = proxy(item.$range, 'input', async (event) => {
|
||||
item.range[0] = item.$range.valueAsNumber
|
||||
item.tooltip = await item.onChange.call(this.art, item, $item, event)
|
||||
})
|
||||
item.$events.push(event)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'selector':
|
||||
{
|
||||
const event = proxy($item, 'click', async (event) => {
|
||||
if (item.selector?.length) {
|
||||
this.render(item.selector)
|
||||
}
|
||||
else {
|
||||
this.check(item)
|
||||
if (item.$parent.onSelect) {
|
||||
item.$parent.tooltip = await item.$parent.onSelect.call(this.art, item, $item, event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
item.$events.push(event)
|
||||
|
||||
if (item.default) {
|
||||
addClass($item, 'art-current')
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'button':
|
||||
if (item.onClick) {
|
||||
const event = proxy($item, 'click', async (event) => {
|
||||
item.tooltip = await item.onClick.call(this.art, item, $item, event)
|
||||
})
|
||||
item.$events.push(event)
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
def(item, '$item', {
|
||||
configurable: true,
|
||||
get: () => $item,
|
||||
})
|
||||
|
||||
if (isUpdate) {
|
||||
replaceElement($item, oldItem)
|
||||
}
|
||||
else {
|
||||
append($panel, $item)
|
||||
}
|
||||
|
||||
if (item.mounted) {
|
||||
timeout(getScope(this.art), () => item.mounted.call(this.art, item.$item, item), 0)
|
||||
}
|
||||
}
|
||||
|
||||
render(option = this.option) {
|
||||
this.active = option
|
||||
if (this.cache.has(option)) {
|
||||
const $panel = this.cache.get(option)
|
||||
inverseClass($panel, 'art-current')
|
||||
}
|
||||
else {
|
||||
const $panel = createElement('div')
|
||||
this.cache.set(option, $panel)
|
||||
addClass($panel, 'art-setting-panel')
|
||||
append(this.$parent, $panel)
|
||||
inverseClass($panel, 'art-current')
|
||||
|
||||
if (option[0]?.$parent) {
|
||||
this.createHeader(option[0])
|
||||
}
|
||||
|
||||
for (let index = 0; index < option.length; index++) {
|
||||
this.createItem(option[index])
|
||||
}
|
||||
}
|
||||
this.resize()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { SettingHost, SettingItem, SettingManager } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { ResourceCleanupError } from '../lifecycle/scope'
|
||||
import { errorHandle } from '../utils'
|
||||
import Component from '../utils/component'
|
||||
import aspectRatio from './aspectRatio'
|
||||
import { installSettingEvents } from './events'
|
||||
import flip from './flip'
|
||||
import { resizeSetting } from './layout'
|
||||
import { findItem, formatTree, registerTreeOwner, releaseTreeOwner, traverseTree } from './model'
|
||||
import playbackRate from './playbackRate'
|
||||
import { beginSettingAdd, cancelSettingAdd } from './registration'
|
||||
import { createSettingHeader, createSettingItem, renderSetting } from './render'
|
||||
import { releaseSettingTree } from './resources'
|
||||
import { checkSetting } from './selection'
|
||||
import subtitleOffset from './subtitleOffset'
|
||||
import { cancelSettingUpdate, updateSetting } from './update'
|
||||
|
||||
// Keep the exact runtime inheritance; Setting overrides the registry operations and cache.
|
||||
const SettingBase: new (art: SettingHost) => Pick<Component<SettingHost>, 'id' | 'art' | 'show' | 'toggle'> = Component
|
||||
|
||||
export default class Setting extends SettingBase implements SettingManager {
|
||||
declare name: string
|
||||
declare $parent: HTMLElement
|
||||
declare cache: Map<SettingItem[], HTMLDivElement>
|
||||
declare active: SettingItem[] | null
|
||||
declare option: SettingItem[]
|
||||
|
||||
constructor(art: SettingHost) {
|
||||
super(art)
|
||||
|
||||
const {
|
||||
option,
|
||||
template: { $setting },
|
||||
} = art
|
||||
|
||||
this.name = 'setting'
|
||||
this.$parent = $setting
|
||||
|
||||
this.id = 0
|
||||
this.active = null
|
||||
this.cache = new Map()
|
||||
this.option = [...this.builtin, ...option.settings]
|
||||
const scope = getScope(art)
|
||||
registerTreeOwner(this, scope)
|
||||
scope.add(() => {
|
||||
releaseTreeOwner(this)
|
||||
})
|
||||
|
||||
if (option.setting) {
|
||||
this.format()
|
||||
this.render()
|
||||
|
||||
installSettingEvents(this)
|
||||
}
|
||||
}
|
||||
|
||||
get builtin(): SettingItem[] {
|
||||
const result: SettingItem[] = []
|
||||
const { option } = this.art
|
||||
|
||||
if (option.playbackRate) {
|
||||
result.push(playbackRate(this.art))
|
||||
}
|
||||
|
||||
if (option.aspectRatio) {
|
||||
result.push(aspectRatio(this.art))
|
||||
}
|
||||
|
||||
if (option.flip) {
|
||||
result.push(flip(this.art))
|
||||
}
|
||||
|
||||
if (option.subtitleOffset) {
|
||||
result.push(subtitleOffset(this.art))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
traverse(callback: (item: SettingItem) => void, option = this.option): void {
|
||||
traverseTree(option, callback)
|
||||
}
|
||||
|
||||
check(target?: SettingItem | null): void {
|
||||
checkSetting(this, target)
|
||||
}
|
||||
|
||||
format(option = this.option, parent?: SettingItem, parents?: SettingItem[], names: string[] = []): void {
|
||||
this.option = formatTree(this, option, parent, parents, names)
|
||||
}
|
||||
|
||||
find(name = ''): SettingItem | null {
|
||||
return findItem(this.option, name)
|
||||
}
|
||||
|
||||
resize(): void {
|
||||
resizeSetting(this)
|
||||
}
|
||||
|
||||
inactivate(item: SettingItem): void {
|
||||
releaseSettingTree(this, item)
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
const item = this.find(name)!
|
||||
errorHandle(item, `Can't find [${name}] in the [setting]`)
|
||||
this.traverse((item) => {
|
||||
cancelSettingAdd(item)
|
||||
cancelSettingUpdate(item)
|
||||
}, [item])
|
||||
const index = item.$option!.indexOf(item)
|
||||
item.$option!.splice(index, 1)
|
||||
const element = item.$item
|
||||
const failures: unknown[] = []
|
||||
for (const cleanup of [() => this.inactivate(item), () => element?.remove(), () => this.render()]) {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof ResourceCleanupError)
|
||||
failures.push(...error.errors)
|
||||
else
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw new ResourceCleanupError(failures)
|
||||
}
|
||||
|
||||
update(target: SettingItem): SettingItem {
|
||||
return updateSetting(this, target)
|
||||
}
|
||||
|
||||
add<Item extends SettingItem>(item: Item, option: SettingItem[] = this.option): Item {
|
||||
if (isClosing(this.art))
|
||||
return item
|
||||
let registered = false
|
||||
this.traverse((existing) => {
|
||||
registered ||= existing === item
|
||||
})
|
||||
const index = option.length
|
||||
const registration = registered ? undefined : beginSettingAdd(item)
|
||||
let formatted = false
|
||||
try {
|
||||
option.push(item)
|
||||
this.format()
|
||||
formatted = true
|
||||
this.createItem(item)
|
||||
this.render()
|
||||
}
|
||||
catch (error) {
|
||||
if (isClosing(this.art) || (registration && !registration.current()))
|
||||
throw error
|
||||
if (option[index] === item)
|
||||
option.splice(index, 1)
|
||||
if (!registered && formatted) {
|
||||
try {
|
||||
this.inactivate(item)
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('ArtPlayer setting cleanup failed:', cleanupError)
|
||||
}
|
||||
item.$item?.remove()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
registration?.finish()
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
createHeader(item: SettingItem): void {
|
||||
createSettingHeader(this, item)
|
||||
}
|
||||
|
||||
createItem(item: SettingItem, isUpdate = false): void {
|
||||
createSettingItem(this, item, isUpdate)
|
||||
}
|
||||
|
||||
render(option = this.option): void {
|
||||
renderSetting(this, option)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { isMobile } from '../utils/compatibility'
|
||||
|
||||
interface LayoutItem {
|
||||
readonly $parent?: { width?: number }
|
||||
}
|
||||
|
||||
export interface SettingLayout {
|
||||
active: LayoutItem[] | null
|
||||
option: LayoutItem[]
|
||||
show: boolean
|
||||
art: {
|
||||
isRotate: boolean
|
||||
controls: { setting?: HTMLElement }
|
||||
constructor: { SETTING_WIDTH: number, SETTING_ITEM_HEIGHT: number }
|
||||
template: { $player: HTMLElement, $setting: HTMLElement, $bottom: HTMLElement }
|
||||
}
|
||||
}
|
||||
|
||||
export interface LayoutInput {
|
||||
containerWidth: number
|
||||
containerHeight: number
|
||||
requestedWidth: number
|
||||
rows: number
|
||||
rowHeight: number
|
||||
controlCenter: number
|
||||
bottom: number
|
||||
padding: number
|
||||
}
|
||||
|
||||
export function calculateSettingLayout(input: LayoutInput): { width: number, height: number, left: number } {
|
||||
const { containerWidth, containerHeight, requestedWidth, rows, rowHeight, controlCenter, bottom, padding } = input
|
||||
const inset = Math.min(Math.max(0, padding), Math.max(0, containerWidth / 2))
|
||||
const width = Math.min(Math.max(0, requestedWidth), Math.max(0, containerWidth - 2 * inset))
|
||||
const height = Math.min(Math.max(0, rows * rowHeight), Math.max(0, containerHeight - bottom - inset))
|
||||
const left = Math.max(inset, Math.min(controlCenter - width / 2, containerWidth - inset - width))
|
||||
return { width, height, left }
|
||||
}
|
||||
|
||||
export function resizeSetting(setting: SettingLayout): void {
|
||||
const { art, active } = setting
|
||||
const { controls, constructor: { SETTING_WIDTH, SETTING_ITEM_HEIGHT }, template: { $player, $setting, $bottom } } = art
|
||||
if (!controls.setting || !setting.show || !active)
|
||||
return
|
||||
const player = $player.getBoundingClientRect()
|
||||
const control = controls.setting.getBoundingClientRect()
|
||||
const scale = player.width / $player.offsetWidth || 1
|
||||
const controlCenter = (control.left - player.left + control.width / 2) / scale - $player.clientLeft
|
||||
const layout = calculateSettingLayout({
|
||||
containerWidth: $player.clientWidth,
|
||||
containerHeight: $player.clientHeight,
|
||||
requestedWidth: active[0]?.$parent?.width || SETTING_WIDTH,
|
||||
rows: active.length + (active === setting.option ? 0 : 1),
|
||||
rowHeight: SETTING_ITEM_HEIGHT,
|
||||
controlCenter,
|
||||
bottom: Number.parseFloat(getComputedStyle($setting).bottom) || 0,
|
||||
padding: Number.parseFloat(getComputedStyle($bottom).paddingLeft) || 0,
|
||||
})
|
||||
$setting.style.height = `${layout.height}px`
|
||||
$setting.style.width = `${layout.width}px`
|
||||
if (art.isRotate || isMobile)
|
||||
return
|
||||
if (controlCenter + layout.width / 2 > $player.clientWidth) {
|
||||
// Preserve the existing CSS right-edge anchor when centering would overflow.
|
||||
$setting.style.left = ''
|
||||
$setting.style.right = ''
|
||||
}
|
||||
else {
|
||||
$setting.style.left = `${layout.left}px`
|
||||
$setting.style.right = 'auto'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { errorHandle } from '../utils/error'
|
||||
|
||||
export interface TreeItem {
|
||||
name?: string
|
||||
selector?: TreeItem[]
|
||||
readonly $parent?: TreeItem
|
||||
readonly $parents?: TreeItem[]
|
||||
readonly $option?: TreeItem[]
|
||||
readonly $events?: (() => void)[]
|
||||
readonly $formatted?: boolean
|
||||
}
|
||||
|
||||
export interface TreeBinding {
|
||||
parent?: TreeItem
|
||||
parents?: TreeItem[]
|
||||
option: TreeItem[]
|
||||
events: (() => void)[]
|
||||
owner: TreeOwnerState
|
||||
}
|
||||
|
||||
interface TreeOwnerState {
|
||||
active: boolean
|
||||
root?: TreeItem[]
|
||||
scope?: { readonly closed: boolean }
|
||||
}
|
||||
|
||||
interface Placement {
|
||||
item: TreeItem
|
||||
parent?: TreeItem
|
||||
parents?: TreeItem[]
|
||||
option: TreeItem[]
|
||||
name: string
|
||||
generated: boolean
|
||||
}
|
||||
|
||||
const bindings = new WeakMap<TreeItem, TreeBinding>()
|
||||
const owners = new WeakMap<object, TreeOwnerState>()
|
||||
const keys = ['$parent', '$parents', '$option', '$events', '$formatted'] as const
|
||||
|
||||
export function hasTreeBinding(item: TreeItem): boolean {
|
||||
return bindings.has(item)
|
||||
}
|
||||
|
||||
export function registerTreeOwner(owner: object, scope: { readonly closed: boolean }): void {
|
||||
owners.set(owner, { active: true, scope })
|
||||
}
|
||||
|
||||
export function releaseTreeOwner(owner: object): void {
|
||||
const state = owners.get(owner)
|
||||
if (state) {
|
||||
state.active = false
|
||||
state.root = undefined
|
||||
state.scope = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function activeBinding(item: TreeItem): boolean {
|
||||
const visited = new Set<TreeItem>()
|
||||
let current: TreeItem | undefined = item
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current)
|
||||
const binding: TreeBinding | undefined = bindings.get(current)
|
||||
if (!binding || !binding.owner.active || binding.owner.scope?.closed || !binding.option.includes(current))
|
||||
return false
|
||||
if (!binding.parent)
|
||||
return binding.owner.root === binding.option
|
||||
if (bindings.get(binding.parent)?.owner !== binding.owner)
|
||||
return false
|
||||
current = binding.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function canAssignName(item: TreeItem): boolean {
|
||||
let target: object | null = item
|
||||
while (target) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, 'name')
|
||||
if (descriptor) {
|
||||
if ('set' in descriptor)
|
||||
return typeof descriptor.set === 'function'
|
||||
return Boolean(descriptor.writable && (target === item || Object.isExtensible(item)))
|
||||
}
|
||||
target = Object.getPrototypeOf(target)
|
||||
}
|
||||
return Object.isExtensible(item)
|
||||
}
|
||||
|
||||
export function treeBinding(item: TreeItem): TreeBinding {
|
||||
const binding = bindings.get(item)
|
||||
if (!binding)
|
||||
throw new Error('Setting item has not been formatted')
|
||||
return binding
|
||||
}
|
||||
|
||||
export function traverseTree(option: TreeItem[], callback: (item: TreeItem) => void): void {
|
||||
for (let index = 0; index < option.length; index++) {
|
||||
const item = option[index]!
|
||||
callback(item)
|
||||
if (item.selector?.length)
|
||||
traverseTree(item.selector, callback)
|
||||
}
|
||||
}
|
||||
|
||||
export function findItem(option: TreeItem[], name = ''): TreeItem | null {
|
||||
let result: TreeItem | null = null
|
||||
traverseTree(option, (item) => {
|
||||
if (item.name === name)
|
||||
result = item
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Validate the complete structure before installing any names or bindings.
|
||||
export function formatTree<Item extends TreeItem>(owner: { id: number }, option: Item[], parent?: TreeItem, parents?: TreeItem[], names: string[] = []): Item[] {
|
||||
const ownerState = owners.get(owner) || { active: true }
|
||||
const placements: Placement[] = []
|
||||
const seen = new Set<TreeItem>()
|
||||
const used = new Set(names)
|
||||
const collect = (list: TreeItem[], parent?: TreeItem, parents?: TreeItem[]) => {
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const item = list[index]!
|
||||
if (!item || typeof item !== 'object')
|
||||
throw new TypeError('Setting item must be an object')
|
||||
if (seen.has(item))
|
||||
throw new Error('Setting items must appear only once in a tree')
|
||||
seen.add(item)
|
||||
const previous = bindings.get(item)
|
||||
if (previous && previous.owner !== ownerState && activeBinding(item))
|
||||
errorHandle(false, `Setting item [${item.name || ''}] already belongs to another active player`)
|
||||
if (item.name) {
|
||||
errorHandle(!used.has(item.name), `The [${item.name}] already exists in [setting]`)
|
||||
used.add(item.name)
|
||||
}
|
||||
if (!bindings.has(item)) {
|
||||
for (const key of keys) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(item, key)
|
||||
if (descriptor ? !descriptor.configurable : !Object.isExtensible(item))
|
||||
throw new TypeError(`Cannot format setting item property [${key}]`)
|
||||
}
|
||||
}
|
||||
if (!item.name && !canAssignName(item))
|
||||
throw new TypeError('Cannot assign an automatic setting item name')
|
||||
placements.push({ item, parent, parents, option: list, name: item.name || '', generated: !item.name })
|
||||
if (item.selector?.length)
|
||||
collect(item.selector, item, list)
|
||||
}
|
||||
}
|
||||
collect(option, parent, parents)
|
||||
let nextId = owner.id
|
||||
owners.set(owner, ownerState)
|
||||
ownerState.root = option
|
||||
for (const placement of placements) {
|
||||
if (!placement.name) {
|
||||
do {
|
||||
placement.name = `setting-${nextId++}`
|
||||
} while (used.has(placement.name))
|
||||
used.add(placement.name)
|
||||
}
|
||||
}
|
||||
for (const placement of placements) {
|
||||
const { item, name, parent, parents, option } = placement
|
||||
const previous = bindings.get(item)
|
||||
if (!previous) {
|
||||
Object.defineProperties(item, {
|
||||
$parent: { get: () => treeBinding(item).parent },
|
||||
$parents: { get: () => treeBinding(item).parents },
|
||||
$option: { get: () => treeBinding(item).option },
|
||||
$events: { get: () => treeBinding(item).events },
|
||||
$formatted: { get: () => true },
|
||||
})
|
||||
}
|
||||
bindings.set(item, { parent, parents, option, events: previous?.events || [], owner: ownerState })
|
||||
if (placement.generated)
|
||||
item.name = name
|
||||
}
|
||||
// Preserve the caller-provided accumulator and monotonic autogenerated IDs.
|
||||
for (const placement of placements) {
|
||||
if (!placement.generated)
|
||||
names.push(placement.name)
|
||||
}
|
||||
owner.id = nextId
|
||||
return option
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { SettingItem, SettingManager } from './types'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
|
||||
const scopes = new WeakMap<HTMLDivElement, ResourceScope>()
|
||||
|
||||
export function ownSettingPanel(setting: SettingManager, option: SettingItem[], panel: HTMLDivElement): ResourceScope {
|
||||
const scope = getScope(setting.art).child()
|
||||
scopes.set(panel, scope)
|
||||
scope.add(() => {
|
||||
if (setting.cache.get(option) === panel)
|
||||
setting.cache.delete(option)
|
||||
panel.remove()
|
||||
})
|
||||
return scope
|
||||
}
|
||||
|
||||
export function settingPanelScope(panel: HTMLDivElement): ResourceScope {
|
||||
const scope = scopes.get(panel)
|
||||
if (!scope)
|
||||
throw new Error('Setting panel has not been registered')
|
||||
return scope
|
||||
}
|
||||
|
||||
export function releaseSettingPanel(panel: HTMLDivElement): void {
|
||||
const scope = scopes.get(panel)
|
||||
if (scope)
|
||||
scope.dispose()
|
||||
else
|
||||
panel.remove()
|
||||
}
|
||||
+8
-5
@@ -1,11 +1,14 @@
|
||||
export default function playbackRate(art) {
|
||||
import type { SettingHost, SettingItem } from './types'
|
||||
import { subscribeSetting } from './resources'
|
||||
|
||||
export default function playbackRate(art: SettingHost): SettingItem {
|
||||
const {
|
||||
i18n,
|
||||
icons,
|
||||
constructor: { SETTING_ITEM_WIDTH, PLAYBACK_RATE },
|
||||
} = art
|
||||
|
||||
function getI18n(value) {
|
||||
function getI18n(value: number) {
|
||||
return value === 1.0 ? i18n.get('Normal') : value.toFixed(1)
|
||||
}
|
||||
|
||||
@@ -29,12 +32,12 @@ export default function playbackRate(art) {
|
||||
}
|
||||
}),
|
||||
onSelect(item) {
|
||||
art.playbackRate = item.value
|
||||
art.playbackRate = item.value as number
|
||||
return item.html
|
||||
},
|
||||
mounted: () => {
|
||||
mounted: (_element, item) => {
|
||||
update()
|
||||
art.on('video:ratechange', () => update())
|
||||
subscribeSetting(art, item, 'video:ratechange', () => update())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SettingItem } from './types'
|
||||
|
||||
const registrations = new WeakMap<SettingItem, object>()
|
||||
|
||||
export function cancelSettingAdd(item: SettingItem): void {
|
||||
registrations.delete(item)
|
||||
}
|
||||
|
||||
export function beginSettingAdd(item: SettingItem): { current: () => boolean, finish: () => void } {
|
||||
const token = {}
|
||||
registrations.set(item, token)
|
||||
const current = () => registrations.get(item) === token
|
||||
return {
|
||||
current,
|
||||
finish() {
|
||||
if (current())
|
||||
registrations.delete(item)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
import type { SettingKind } from './selection'
|
||||
import type { SettingItem, SettingManager, SettingRange } from './types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import { addClass, append, def, has, inverseClass, setStyle } from '../utils'
|
||||
import { settingScopeActive } from './activity'
|
||||
import { captureSettingItem, rememberSettingState } from './checkpoint'
|
||||
import { ownSettingPanel, releaseSettingPanel, settingPanelScope } from './panels'
|
||||
import { ownSettingItem, proxySetting, settingItemOwner } from './resources'
|
||||
import { bindSettingActions } from './selection'
|
||||
import { captureSettingUpdate } from './update'
|
||||
|
||||
const navigations = new WeakMap<SettingManager, number>()
|
||||
|
||||
function bindContent(item: SettingItem, key: 'icon' | 'html' | 'tooltip', element: HTMLDivElement): void {
|
||||
def(item, `$${key}`, { configurable: true, get: () => element })
|
||||
def(item, key, {
|
||||
configurable: true,
|
||||
get: () => element.innerHTML,
|
||||
set(value: unknown) {
|
||||
element.innerHTML = ''
|
||||
append(element, value)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createSettingHeader(setting: SettingManager, item: SettingItem): void {
|
||||
if (!setting.cache.has(item.$option!))
|
||||
return
|
||||
const $panel = setting.cache.get(item.$option!)!
|
||||
|
||||
const {
|
||||
icons: { arrowLeft },
|
||||
constructor: { SETTING_ITEM_HEIGHT },
|
||||
} = setting.art
|
||||
|
||||
const $item = document.createElement('div')
|
||||
setStyle($item, 'height', `${SETTING_ITEM_HEIGHT}px`)
|
||||
addClass($item, 'art-setting-item')
|
||||
addClass($item, 'art-setting-item-back')
|
||||
const $left = append($item, '<div class="art-setting-item-left"></div>')
|
||||
const $icon = document.createElement('div')
|
||||
addClass($icon, 'art-setting-item-left-icon')
|
||||
append($icon, arrowLeft)
|
||||
append($left, $icon)
|
||||
append($left, item.$parent!.html)
|
||||
proxySetting(setting.art, item.$parent!, $item, 'click', () => setting.render(item.$parents), settingPanelScope($panel))
|
||||
append($panel, $item)
|
||||
}
|
||||
|
||||
export function createSettingItem(setting: SettingManager, item: SettingItem, isUpdate = false): void {
|
||||
const currentUpdate = captureSettingUpdate(item)
|
||||
if (!setting.cache.has(item.$option!))
|
||||
return
|
||||
const $panel = setting.cache.get(item.$option!)!
|
||||
const oldItem = item.$item
|
||||
|
||||
let type: SettingKind = 'selector'
|
||||
|
||||
if (has(item, 'switch')) {
|
||||
type = 'switch'
|
||||
}
|
||||
|
||||
if (has(item, 'range')) {
|
||||
type = 'range'
|
||||
}
|
||||
|
||||
if (has(item, 'onClick')) {
|
||||
type = 'button'
|
||||
}
|
||||
|
||||
const { icons, constructor } = setting.art
|
||||
|
||||
if (!currentUpdate())
|
||||
return
|
||||
const scope = ownSettingItem(setting.art, item, settingPanelScope($panel))
|
||||
const current = () => settingScopeActive(scope) && !isClosing(setting.art) && currentUpdate()
|
||||
if (!current())
|
||||
return
|
||||
const $item = document.createElement('div')
|
||||
try {
|
||||
rememberSettingState(item)
|
||||
addClass($item, 'art-setting-item')
|
||||
setStyle($item, 'height', `${constructor.SETTING_ITEM_HEIGHT}px`)
|
||||
|
||||
$item.dataset.name = item.name || ''
|
||||
$item.dataset.value = String(item.value || '')
|
||||
if (!current())
|
||||
return
|
||||
|
||||
const $left = append($item, '<div class="art-setting-item-left"></div>')
|
||||
const $right = append($item, '<div class="art-setting-item-right"></div>')
|
||||
|
||||
const $icon = document.createElement('div')
|
||||
addClass($icon, 'art-setting-item-left-icon')
|
||||
|
||||
switch (type) {
|
||||
case 'button':
|
||||
case 'switch':
|
||||
case 'range': {
|
||||
const icon = item.icon || icons.config
|
||||
if (!current())
|
||||
return
|
||||
append($icon, icon)
|
||||
break
|
||||
}
|
||||
case 'selector':
|
||||
if (item.selector?.length) {
|
||||
const icon = item.icon || icons.config
|
||||
if (!current())
|
||||
return
|
||||
append($icon, icon)
|
||||
}
|
||||
else {
|
||||
append($icon, icons.check)
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
append($left, $icon)
|
||||
|
||||
if (!current())
|
||||
return
|
||||
bindContent(item, 'icon', $icon)
|
||||
|
||||
const $html = document.createElement('div')
|
||||
addClass($html, 'art-setting-item-left-text')
|
||||
const html = item.html || ''
|
||||
if (!current())
|
||||
return
|
||||
append($html, html)
|
||||
append($left, $html)
|
||||
|
||||
bindContent(item, 'html', $html)
|
||||
|
||||
const $tooltip = document.createElement('div')
|
||||
addClass($tooltip, 'art-setting-item-right-tooltip')
|
||||
const tooltip = item.tooltip || ''
|
||||
if (!current())
|
||||
return
|
||||
append($tooltip, tooltip)
|
||||
append($right, $tooltip)
|
||||
|
||||
bindContent(item, 'tooltip', $tooltip)
|
||||
|
||||
switch (type) {
|
||||
case 'switch': {
|
||||
const $switch = document.createElement('div')
|
||||
addClass($switch, 'art-setting-item-right-icon')
|
||||
const $switchOn = append($switch, icons.switchOn)
|
||||
const $switchOff = append($switch, icons.switchOff)
|
||||
const initialSwitch = item.switch
|
||||
if (!current())
|
||||
return
|
||||
setStyle(initialSwitch ? $switchOff : $switchOn, 'display', 'none')
|
||||
append($right, $switch)
|
||||
|
||||
def(item, '$switch', {
|
||||
configurable: true,
|
||||
get: () => $switch,
|
||||
})
|
||||
|
||||
let $switchValue = item.switch
|
||||
if (!current())
|
||||
return
|
||||
rememberSettingState(item, () => {
|
||||
const value = $switchValue
|
||||
return () => {
|
||||
$switchValue = value
|
||||
}
|
||||
})
|
||||
def(item, 'switch', {
|
||||
configurable: true,
|
||||
get: () => $switchValue,
|
||||
set(value) {
|
||||
$switchValue = value
|
||||
if (value) {
|
||||
setStyle($switchOff, 'display', 'none')
|
||||
setStyle($switchOn, 'display', null)
|
||||
}
|
||||
else {
|
||||
setStyle($switchOff, 'display', null)
|
||||
setStyle($switchOn, 'display', 'none')
|
||||
}
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'range':
|
||||
{
|
||||
const $state = document.createElement('div')
|
||||
addClass($state, 'art-setting-item-right-icon')
|
||||
const $range = document.createElement('input')
|
||||
$range.type = 'range'
|
||||
append($state, $range)
|
||||
for (const [index, key] of ['value', 'min', 'max', 'step'].entries()) {
|
||||
const value = item.range![index]
|
||||
if (!current())
|
||||
return
|
||||
Reflect.set($range, key, String(value))
|
||||
}
|
||||
addClass($range, 'art-setting-range')
|
||||
append($right, $state)
|
||||
|
||||
def(item, '$range', {
|
||||
configurable: true,
|
||||
get: () => $range,
|
||||
})
|
||||
|
||||
let $rangeValue = [...item.range!]
|
||||
if (!current())
|
||||
return
|
||||
rememberSettingState(item, () => {
|
||||
const value = $rangeValue
|
||||
const values = [...value]
|
||||
const input = { min: $range.min, max: $range.max, step: $range.step, value: $range.value }
|
||||
return () => {
|
||||
value.splice(0, value.length, ...values)
|
||||
$rangeValue = value
|
||||
Object.assign($range, input)
|
||||
}
|
||||
})
|
||||
def(item, 'range', {
|
||||
configurable: true,
|
||||
get: () => $rangeValue,
|
||||
set(value: SettingRange) {
|
||||
$rangeValue = [...value]
|
||||
$range.value = String(value[0])
|
||||
$range.min = String(value[1])
|
||||
$range.max = String(value[2])
|
||||
$range.step = String(value[3])
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'selector':
|
||||
if (item.selector?.length) {
|
||||
const $state = document.createElement('div')
|
||||
addClass($state, 'art-setting-item-right-icon')
|
||||
append($state, icons.arrowRight)
|
||||
append($right, $state)
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!current())
|
||||
return
|
||||
bindSettingActions(setting, item, $item, type)
|
||||
if (!current())
|
||||
return
|
||||
|
||||
def(item, '$item', {
|
||||
configurable: true,
|
||||
get: () => $item,
|
||||
})
|
||||
|
||||
if (isUpdate) {
|
||||
if (oldItem?.parentNode)
|
||||
oldItem.replaceWith($item)
|
||||
else
|
||||
append($panel, $item)
|
||||
}
|
||||
else {
|
||||
append($panel, $item)
|
||||
}
|
||||
|
||||
if (item.mounted && current()) {
|
||||
timeout(scope, () => {
|
||||
if (isClosing(setting.art))
|
||||
return
|
||||
try {
|
||||
const result = item.mounted?.call(setting.art, $item, item)
|
||||
void Promise.resolve(result).catch(error => console.warn('ArtPlayer setting mounted failed:', error))
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('ArtPlayer setting mounted failed:', error)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
scope.dispose()
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('ArtPlayer setting cleanup failed:', cleanupError)
|
||||
}
|
||||
$item.remove()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function renderSetting(setting: SettingManager, option = setting.option): void {
|
||||
if (isClosing(setting.art))
|
||||
return
|
||||
const cached = setting.cache.get(option)
|
||||
if (cached && !settingScopeActive(settingPanelScope(cached)))
|
||||
return
|
||||
const navigation = (navigations.get(setting) || 0) + 1
|
||||
navigations.set(setting, navigation)
|
||||
const currentNavigation = () => navigations.get(setting) === navigation && !isClosing(setting.art)
|
||||
const previous = setting.active
|
||||
const previousLayout = ['height', 'width', 'left', 'right'].map(key => [key, setting.$parent.style.getPropertyValue(key), setting.$parent.style.getPropertyPriority(key)] as const)
|
||||
const restoreNavigation = () => {
|
||||
setting.active = previous
|
||||
if (previous && setting.cache.has(previous))
|
||||
inverseClass(setting.cache.get(previous), 'art-current')
|
||||
for (const [key, value, priority] of previousLayout)
|
||||
setting.$parent.style.setProperty(key, value, priority)
|
||||
}
|
||||
setting.active = option
|
||||
if (cached) {
|
||||
try {
|
||||
inverseClass(cached, 'art-current')
|
||||
setting.resize()
|
||||
}
|
||||
catch (error) {
|
||||
if (currentNavigation() && setting.cache.get(option) === cached && !settingPanelScope(cached).closed)
|
||||
restoreNavigation()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
else {
|
||||
const $panel = document.createElement('div')
|
||||
setting.cache.set(option, $panel)
|
||||
const scope = ownSettingPanel(setting, option, $panel)
|
||||
const current = () => !scope.closed && !isClosing(setting.art) && setting.cache.get(option) === $panel
|
||||
const checkpoints: { item: SettingItem, restore: () => void }[] = []
|
||||
try {
|
||||
if (!current())
|
||||
return
|
||||
addClass($panel, 'art-setting-panel')
|
||||
append(setting.$parent, $panel)
|
||||
inverseClass($panel, 'art-current')
|
||||
|
||||
if (option[0]?.$parent)
|
||||
setting.createHeader(option[0])
|
||||
|
||||
for (let index = 0; index < option.length && current(); index++) {
|
||||
const item = option[index]!
|
||||
checkpoints.push({ item, restore: captureSettingItem(item) })
|
||||
setting.createItem(item)
|
||||
}
|
||||
if (!current())
|
||||
return
|
||||
setting.resize()
|
||||
}
|
||||
catch (error) {
|
||||
const restore = current()
|
||||
const restoreActive = setting.active === option && currentNavigation()
|
||||
try {
|
||||
releaseSettingPanel($panel)
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('ArtPlayer setting cleanup failed:', cleanupError)
|
||||
}
|
||||
if (restore && !isClosing(setting.art)) {
|
||||
for (const checkpoint of checkpoints.reverse()) {
|
||||
if (settingItemOwner(checkpoint.item) === scope) {
|
||||
try {
|
||||
checkpoint.restore()
|
||||
}
|
||||
catch (restoreError) {
|
||||
console.warn('ArtPlayer setting restore failed:', restoreError)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (restoreActive)
|
||||
restoreNavigation()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { SettingEvents, SettingHost, SettingItem, SettingManager } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { ResourceCleanupError } from '../lifecycle/scope'
|
||||
import { pauseSettingScope, settingScopeActive } from './activity'
|
||||
import { hasTreeBinding, treeBinding } from './model'
|
||||
import { releaseSettingPanel } from './panels'
|
||||
|
||||
const scopes = new WeakMap<SettingItem, ResourceScope>()
|
||||
const owners = new WeakMap<SettingItem, ResourceScope>()
|
||||
const eventLists = new WeakMap<ResourceScope, (() => void)[]>()
|
||||
|
||||
function releaseEvents(art: SettingHost, events: (() => void)[]): void {
|
||||
const pending = events.splice(0)
|
||||
const failures: unknown[] = []
|
||||
for (const cleanup of pending) {
|
||||
try {
|
||||
art.events.remove(cleanup)
|
||||
}
|
||||
catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw new ResourceCleanupError(failures)
|
||||
}
|
||||
|
||||
export function ownSettingItem(art: SettingHost, item: SettingItem, owner = getScope(art)): ResourceScope {
|
||||
const previous = scopes.get(item)
|
||||
if (!previous || settingScopeActive(previous) || previous.closed)
|
||||
releaseSettingItem(art, item)
|
||||
const scope = owner.child()
|
||||
scopes.set(item, scope)
|
||||
owners.set(item, owner)
|
||||
eventLists.set(scope, treeBinding(item).events)
|
||||
scope.add(() => {
|
||||
releaseEvents(art, eventLists.get(scope)!)
|
||||
})
|
||||
return scope
|
||||
}
|
||||
|
||||
export function settingItemOwner(item: SettingItem): ResourceScope | undefined {
|
||||
return owners.get(item)
|
||||
}
|
||||
|
||||
export function settingScope(item: SettingItem): ResourceScope {
|
||||
const scope = scopes.get(item)
|
||||
if (!scope)
|
||||
throw new Error('Setting item has not been rendered')
|
||||
return scope
|
||||
}
|
||||
|
||||
export function releaseSettingItem(art: SettingHost, item: SettingItem): void {
|
||||
const scope = scopes.get(item)
|
||||
if (scope && !scope.closed)
|
||||
scope.dispose()
|
||||
else if (hasTreeBinding(item))
|
||||
releaseEvents(art, treeBinding(item).events)
|
||||
}
|
||||
|
||||
export interface SettingSuspension {
|
||||
resume: () => void
|
||||
dispose: () => void
|
||||
}
|
||||
|
||||
export function suspendSettingItem(art: SettingHost, item: SettingItem): SettingSuspension | undefined {
|
||||
const previous = scopes.get(item)
|
||||
if (!previous || previous.closed)
|
||||
return
|
||||
if (!settingScopeActive(previous))
|
||||
throw new Error('Setting item update is already in progress')
|
||||
const owner = owners.get(item)!
|
||||
const events = treeBinding(item).events
|
||||
const saved = events.splice(0)
|
||||
eventLists.set(previous, saved)
|
||||
const unpause = pauseSettingScope(previous)
|
||||
let pending = true
|
||||
return {
|
||||
resume() {
|
||||
if (!pending)
|
||||
return
|
||||
pending = false
|
||||
const failures: unknown[] = []
|
||||
const current = scopes.get(item)
|
||||
for (const cleanup of [() => {
|
||||
if (current && current !== previous)
|
||||
current.dispose()
|
||||
}, () => releaseEvents(art, events)]) {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof ResourceCleanupError)
|
||||
failures.push(...error.errors)
|
||||
else
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
if (!previous.closed && !isClosing(art)) {
|
||||
events.push(...saved.splice(0))
|
||||
eventLists.set(previous, events)
|
||||
scopes.set(item, previous)
|
||||
owners.set(item, owner)
|
||||
}
|
||||
unpause()
|
||||
if (failures.length)
|
||||
throw new ResourceCleanupError(failures)
|
||||
},
|
||||
dispose() {
|
||||
if (!pending)
|
||||
return
|
||||
pending = false
|
||||
try {
|
||||
previous.dispose()
|
||||
}
|
||||
finally {
|
||||
unpause()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function proxySetting(art: SettingHost, item: SettingItem, target: EventTarget, name: string, callback: (event: Event) => unknown, owner?: ResourceScope): void {
|
||||
const scope = owner || settingScope(item)
|
||||
if (!settingScopeActive(scope) || isClosing(art))
|
||||
return
|
||||
const event = art.proxy(target, name, (event) => {
|
||||
if (settingScopeActive(scope) && !isClosing(art)) {
|
||||
try {
|
||||
return callback(event)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('ArtPlayer setting callback failed:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!settingScopeActive(scope) || isClosing(art)) {
|
||||
art.events.remove(event)
|
||||
}
|
||||
else {
|
||||
treeBinding(item).events.push(event)
|
||||
if (owner && owner !== scopes.get(item)) {
|
||||
owner.add(() => {
|
||||
const events = treeBinding(item).events
|
||||
const index = events.indexOf(event)
|
||||
if (index >= 0) {
|
||||
events.splice(index, 1)
|
||||
art.events.remove(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeSetting<Name extends keyof SettingEvents>(art: SettingHost, item: SettingItem, name: Name, callback: (...args: SettingEvents[Name]) => unknown): void {
|
||||
const scope = settingScope(item)
|
||||
if (!settingScopeActive(scope) || isClosing(art))
|
||||
return
|
||||
const guarded = (...args: SettingEvents[Name]) => {
|
||||
if (settingScopeActive(scope) && !isClosing(art))
|
||||
return callback(...args)
|
||||
}
|
||||
art.on(name, guarded)
|
||||
scope.add(() => {
|
||||
art.off(name, guarded)
|
||||
})
|
||||
}
|
||||
|
||||
export function releaseSettingTree(setting: SettingManager, item: SettingItem): void {
|
||||
const failures: unknown[] = []
|
||||
const visited = new Set<SettingItem>()
|
||||
const visit = (item: SettingItem) => {
|
||||
if (visited.has(item))
|
||||
return
|
||||
visited.add(item)
|
||||
for (const child of item.selector || [])
|
||||
visit(child)
|
||||
if (item.selector) {
|
||||
const panel = setting.cache.get(item.selector)
|
||||
setting.cache.delete(item.selector)
|
||||
try {
|
||||
if (panel)
|
||||
releaseSettingPanel(panel)
|
||||
}
|
||||
catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
try {
|
||||
releaseSettingItem(setting.art, item)
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof ResourceCleanupError)
|
||||
failures.push(...error.errors)
|
||||
else
|
||||
failures.push(error)
|
||||
}
|
||||
}
|
||||
visit(item)
|
||||
if (failures.length)
|
||||
throw new ResourceCleanupError(failures)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { SettingCallback, SettingItem, SettingManager } from './types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { addClass, inverseClass } from '../utils/dom'
|
||||
import { settingScopeActive } from './activity'
|
||||
import { proxySetting, settingScope } from './resources'
|
||||
|
||||
export type SettingKind = 'button' | 'selector' | 'switch' | 'range'
|
||||
const generations = new WeakMap<SettingItem, number>()
|
||||
|
||||
export function checkSetting(setting: SettingManager, target?: SettingItem | null): void {
|
||||
if (!target?.$parent)
|
||||
return
|
||||
target.$parent.tooltip = target.html
|
||||
setting.traverse((item) => {
|
||||
item.default = item === target
|
||||
if (item.default && item.$item)
|
||||
inverseClass(item.$item, 'art-current')
|
||||
}, target.$option)
|
||||
setting.render(target.$parents)
|
||||
}
|
||||
|
||||
async function act(setting: SettingManager, item: SettingItem, element: HTMLDivElement, event: Event, callback: SettingCallback, target: SettingItem, property: 'switch' | 'tooltip', before?: () => void): Promise<void> {
|
||||
try {
|
||||
const scope = settingScope(item)
|
||||
const targetScope = settingScope(target)
|
||||
const generation = (generations.get(target) || 0) + 1
|
||||
generations.set(target, generation)
|
||||
const active = () => settingScopeActive(scope) && settingScopeActive(targetScope) && !isClosing(setting.art) && generations.get(target) === generation
|
||||
before?.()
|
||||
if (!active())
|
||||
return
|
||||
const value = await callback.call(setting.art, item, element, event)
|
||||
if (active())
|
||||
Reflect.set(target, property, value)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('ArtPlayer setting callback failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function bindSettingActions(setting: SettingManager, item: SettingItem, element: HTMLDivElement, kind: SettingKind): void {
|
||||
const { art } = setting
|
||||
const scope = settingScope(item)
|
||||
const proxy = (target: EventTarget, name: string, callback: (event: Event) => unknown) => proxySetting(art, item, target, name, callback, scope)
|
||||
switch (kind) {
|
||||
case 'switch':
|
||||
if (item.onSwitch)
|
||||
proxy(element, 'click', event => act(setting, item, element, event, item.onSwitch!, item, 'switch'))
|
||||
break
|
||||
case 'range': {
|
||||
const range = item.$range
|
||||
if (range) {
|
||||
for (const [name, callback] of [['change', 'onRange'], ['input', 'onChange']] as const) {
|
||||
if (item[callback]) {
|
||||
proxy(range, name, event => act(setting, item, element, event, item[callback]!, item, 'tooltip', () => {
|
||||
item.range![0] = range.valueAsNumber
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'selector':
|
||||
proxy(element, 'click', (event) => {
|
||||
if (item.selector?.length) {
|
||||
setting.render(item.selector)
|
||||
}
|
||||
else if (item.$parent?.onSelect) {
|
||||
return act(setting, item, element, event, item.$parent.onSelect, item.$parent, 'tooltip', () => setting.check(item))
|
||||
}
|
||||
else {
|
||||
setting.check(item)
|
||||
}
|
||||
})
|
||||
if (item.default)
|
||||
addClass(element, 'art-current')
|
||||
break
|
||||
case 'button':
|
||||
if (item.onClick)
|
||||
proxy(element, 'click', event => act(setting, item, element, event, item.onClick!, item, 'tooltip'))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export default function subtitleOffset(art) {
|
||||
const { i18n, icons, constructor } = art
|
||||
|
||||
return {
|
||||
width: constructor.SETTING_ITEM_WIDTH,
|
||||
name: 'subtitle-offset',
|
||||
html: i18n.get('Subtitle Offset'),
|
||||
icon: icons.subtitle,
|
||||
tooltip: '0s',
|
||||
range: [0, -10, 10, 0.1],
|
||||
onChange(item) {
|
||||
art.subtitleOffset = item.range[0]
|
||||
return `${item.range[0]}s`
|
||||
},
|
||||
mounted: (_, item) => {
|
||||
art.on('subtitleOffset', (value) => {
|
||||
item.$range.value = value
|
||||
item.tooltip = `${value}s`
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { SettingHost, SettingItem } from './types'
|
||||
import { subscribeSetting } from './resources'
|
||||
|
||||
export default function subtitleOffset(art: SettingHost): SettingItem {
|
||||
const { i18n, icons, constructor } = art
|
||||
|
||||
return {
|
||||
width: constructor.SETTING_ITEM_WIDTH,
|
||||
name: 'subtitle-offset',
|
||||
html: i18n.get('Subtitle Offset'),
|
||||
icon: icons.subtitle,
|
||||
tooltip: '0s',
|
||||
range: [0, -10, 10, 0.1],
|
||||
onChange(item) {
|
||||
art.subtitleOffset = item.range![0]!
|
||||
return `${item.range![0]}s`
|
||||
},
|
||||
mounted: (_, item) => {
|
||||
subscribeSetting(art, item, 'subtitleOffset', (value) => {
|
||||
item.$range!.value = String(value)
|
||||
item.tooltip = `${value}s`
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { ComponentHost } from '../component/types'
|
||||
import type { TreeItem } from './model'
|
||||
|
||||
export type SettingContent = string | HTMLElement | number
|
||||
export type SettingRange = [value?: number, min?: number, max?: number, step?: number]
|
||||
export type SettingCallback = (this: SettingHost, item: SettingItem, element: HTMLDivElement, event: Event) => unknown
|
||||
|
||||
export interface SettingItem extends TreeItem {
|
||||
html?: SettingContent
|
||||
icon?: SettingContent
|
||||
tooltip?: SettingContent
|
||||
width?: number
|
||||
value?: string | number
|
||||
default?: boolean
|
||||
switch?: boolean
|
||||
range?: SettingRange
|
||||
selector?: SettingItem[]
|
||||
mounted?: (this: SettingHost, element: HTMLDivElement, item: SettingItem) => unknown
|
||||
onClick?: SettingCallback
|
||||
onSwitch?: SettingCallback
|
||||
onRange?: SettingCallback
|
||||
onChange?: SettingCallback
|
||||
onSelect?: SettingCallback
|
||||
readonly $parent?: SettingItem
|
||||
readonly $parents?: SettingItem[]
|
||||
readonly $option?: SettingItem[]
|
||||
readonly $item?: HTMLDivElement
|
||||
readonly $icon?: HTMLDivElement
|
||||
readonly $html?: HTMLDivElement
|
||||
readonly $tooltip?: HTMLDivElement
|
||||
readonly $switch?: HTMLDivElement
|
||||
readonly $range?: HTMLInputElement
|
||||
}
|
||||
|
||||
export interface SettingEvents {
|
||||
'blur': []
|
||||
'focus': [event: Event]
|
||||
'resize': []
|
||||
'video:ratechange': []
|
||||
'aspectRatio': [value: string]
|
||||
'flip': [value: string]
|
||||
'subtitleOffset': [value: number]
|
||||
}
|
||||
|
||||
export interface SettingHost extends ComponentHost, SubscriptionHost<SettingEvents> {
|
||||
template: { $player: HTMLElement, $setting: HTMLElement, $bottom: HTMLElement, $controls: HTMLElement }
|
||||
controls: { setting?: HTMLElement }
|
||||
option: { setting: boolean, settings: SettingItem[], playbackRate: boolean, aspectRatio: boolean, flip: boolean, subtitleOffset: boolean }
|
||||
constructor: { SETTING_WIDTH: number, SETTING_ITEM_HEIGHT: number, SETTING_ITEM_WIDTH: number, PLAYBACK_RATE: number[], ASPECT_RATIO: string[], FLIP: string[] }
|
||||
icons: Record<'arrowLeft' | 'arrowRight' | 'config' | 'check' | 'switchOn' | 'switchOff' | 'playbackRate' | 'aspectRatio' | 'flip' | 'subtitle', HTMLElement>
|
||||
proxy: ComponentHost['events']['proxy']
|
||||
i18n: { get: (key: string) => string }
|
||||
setting: SettingManager
|
||||
isRotate: boolean
|
||||
playbackRate: number
|
||||
aspectRatio: string
|
||||
flip: string
|
||||
subtitleOffset: number
|
||||
}
|
||||
|
||||
export interface SettingManager {
|
||||
id: number
|
||||
art: SettingHost
|
||||
name: string
|
||||
$parent: HTMLElement
|
||||
cache: Map<SettingItem[], HTMLDivElement>
|
||||
active: SettingItem[] | null
|
||||
option: SettingItem[]
|
||||
show: boolean
|
||||
traverse: (callback: (item: SettingItem) => void, option?: SettingItem[]) => void
|
||||
check: (target?: SettingItem | null) => void
|
||||
render: (option?: SettingItem[]) => void
|
||||
resize: () => void
|
||||
createItem: (item: SettingItem, isUpdate?: boolean) => void
|
||||
createHeader: (item: SettingItem) => void
|
||||
find: (name?: string) => SettingItem | null
|
||||
format: () => void
|
||||
add: <Item extends SettingItem>(item: Item, option?: SettingItem[]) => Item
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import type { SettingItem, SettingManager } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { ResourceCleanupError } from '../lifecycle/scope'
|
||||
import captureTemplate from '../lifecycle/template-rollback'
|
||||
import { inverseClass } from '../utils/dom'
|
||||
import { pauseSettingScope } from './activity'
|
||||
import { captureSettingItem } from './checkpoint'
|
||||
import { releaseSettingPanel, settingPanelScope } from './panels'
|
||||
import { cancelSettingAdd } from './registration'
|
||||
import { suspendSettingItem } from './resources'
|
||||
|
||||
const pending = new WeakMap<SettingItem, () => void>()
|
||||
|
||||
function finish(cleanups: (() => void)[]): void {
|
||||
const errors: unknown[] = []
|
||||
for (const cleanup of cleanups) {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof ResourceCleanupError)
|
||||
errors.push(...error.errors)
|
||||
else
|
||||
errors.push(error)
|
||||
}
|
||||
}
|
||||
if (errors.length)
|
||||
throw new ResourceCleanupError(errors)
|
||||
}
|
||||
|
||||
function suspendTree(setting: SettingManager, root: SettingItem): { items: Set<SettingItem>, resume: () => void, dispose: () => void } {
|
||||
const items = new Set<SettingItem>()
|
||||
const panels = new Set<HTMLDivElement>()
|
||||
const collect = (item: SettingItem) => {
|
||||
if (items.has(item))
|
||||
return
|
||||
items.add(item)
|
||||
if (item.selector) {
|
||||
const panel = setting.cache.get(item.selector)
|
||||
if (panel)
|
||||
panels.add(panel)
|
||||
item.selector.forEach(collect)
|
||||
}
|
||||
}
|
||||
collect(root)
|
||||
const suspended: NonNullable<ReturnType<typeof suspendSettingItem>>[] = []
|
||||
const unpause: (() => void)[] = []
|
||||
try {
|
||||
for (const item of items) {
|
||||
const suspension = suspendSettingItem(setting.art, item)
|
||||
if (suspension)
|
||||
suspended.push(suspension)
|
||||
}
|
||||
for (const panel of panels)
|
||||
unpause.push(pauseSettingScope(settingPanelScope(panel)))
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
finish([...suspended.reverse().map(item => item.resume), ...unpause])
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('ArtPlayer setting restore failed:', cleanupError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
let active = true
|
||||
return {
|
||||
items,
|
||||
resume() {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
finish([...suspended.reverse().map(item => item.resume), ...unpause])
|
||||
},
|
||||
dispose() {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
finish([...suspended.reverse().map(item => item.dispose), ...Array.from(panels).reverse().map(panel => () => releaseSettingPanel(panel)), ...unpause])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// A newer operation restores the pending one before applying its own changes.
|
||||
export function cancelSettingUpdate(item: SettingItem): void {
|
||||
pending.get(item)?.()
|
||||
}
|
||||
|
||||
export function captureSettingUpdate(item: SettingItem): () => boolean {
|
||||
const operation = pending.get(item)
|
||||
return () => !operation || pending.get(item) === operation
|
||||
}
|
||||
|
||||
function assignSetting(item: SettingItem, target: SettingItem, current: () => boolean): void {
|
||||
for (const key of Reflect.ownKeys(target)) {
|
||||
if (!current())
|
||||
return
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, key)
|
||||
if (!descriptor?.enumerable || !current())
|
||||
continue
|
||||
const value: unknown = Reflect.get(target, key)
|
||||
if (!current())
|
||||
return
|
||||
if (!Reflect.set(item, key, value))
|
||||
throw new TypeError(`Cannot assign setting item property [${String(key)}]`)
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSetting(setting: SettingManager, target: SettingItem): SettingItem {
|
||||
const item = setting.find(target.name)
|
||||
if (isClosing(setting.art))
|
||||
return item || target
|
||||
if (!item)
|
||||
return setting.add(target)
|
||||
cancelSettingAdd(item)
|
||||
cancelSettingUpdate(item)
|
||||
const restoreItem = captureSettingItem(item)
|
||||
const element = item.$item
|
||||
const parent = element?.parentNode
|
||||
const next = element?.nextSibling
|
||||
const restoreTemplate = element ? captureTemplate(element) : undefined
|
||||
const previous = setting.active
|
||||
const layout = ['height', 'width', 'left', 'right'].map(key => [key, setting.$parent.style.getPropertyValue(key), setting.$parent.style.getPropertyPriority(key)] as const)
|
||||
const tree = suspendTree(setting, item)
|
||||
let active = true
|
||||
let rendered = false
|
||||
const cancel = () => {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
for (const affected of tree.items) {
|
||||
if (pending.get(affected) === cancel)
|
||||
pending.delete(affected)
|
||||
}
|
||||
if (isClosing(setting.art)) {
|
||||
tree.dispose()
|
||||
return
|
||||
}
|
||||
const replacement = item.$item
|
||||
try {
|
||||
finish([
|
||||
tree.resume,
|
||||
() => {
|
||||
if (replacement && replacement !== element)
|
||||
replacement.remove()
|
||||
},
|
||||
restoreItem,
|
||||
() => restoreTemplate?.(),
|
||||
() => {
|
||||
if (parent && element)
|
||||
parent.insertBefore(element, next?.parentNode === parent ? next : null)
|
||||
},
|
||||
() => setting.format(),
|
||||
() => {
|
||||
if (rendered) {
|
||||
setting.active = previous
|
||||
if (previous && setting.cache.has(previous))
|
||||
inverseClass(setting.cache.get(previous), 'art-current')
|
||||
for (const [key, value, priority] of layout)
|
||||
setting.$parent.style.setProperty(key, value, priority)
|
||||
}
|
||||
},
|
||||
])
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('ArtPlayer setting restore failed:', error)
|
||||
}
|
||||
}
|
||||
const current = () => active && !isClosing(setting.art) && pending.get(item) === cancel
|
||||
for (const affected of tree.items)
|
||||
pending.set(affected, cancel)
|
||||
const release = getScope(setting.art).add(() => {
|
||||
active = false
|
||||
for (const affected of tree.items) {
|
||||
if (pending.get(affected) === cancel)
|
||||
pending.delete(affected)
|
||||
}
|
||||
tree.dispose()
|
||||
})
|
||||
try {
|
||||
assignSetting(item, target, current)
|
||||
if (!current())
|
||||
return item
|
||||
setting.format()
|
||||
if (!current())
|
||||
return item
|
||||
setting.createItem(item, true)
|
||||
if (!current())
|
||||
return item
|
||||
rendered = true
|
||||
setting.render()
|
||||
if (!current())
|
||||
return item
|
||||
active = false
|
||||
for (const affected of tree.items) {
|
||||
if (pending.get(affected) === cancel)
|
||||
pending.delete(affected)
|
||||
}
|
||||
tree.dispose()
|
||||
return item
|
||||
}
|
||||
catch (error) {
|
||||
if (current())
|
||||
cancel()
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
@@ -50,28 +51,45 @@
|
||||
|
||||
.art-setting-item-left {
|
||||
.art-center();
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
gap: 5px;
|
||||
|
||||
.art-setting-item-left-icon {
|
||||
.art-center();
|
||||
flex-shrink: 0;
|
||||
height: var(--art-settings-icon-size);
|
||||
width: var(--art-settings-icon-size);
|
||||
}
|
||||
|
||||
.art-setting-item-left-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.art-setting-item-right {
|
||||
.art-center();
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
max-width: 55%;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
|
||||
.art-setting-item-right-tooltip {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.art-setting-item-right-icon {
|
||||
.art-center();
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user