refactor(core): CORE-18 type builtins and own prompt lifetimes

This commit is contained in:
Harvey Zhao committed 2026-09-11 12:31:01 +08:00
1 parent a842058136
commit 98a1800032
55 files changed
+26511 -650

No files matched your search

+65 -1
View File
@@ -4,6 +4,69 @@ 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.
## Builtins and prompts (CORE-18)
`src/notice.ts` owns the notice timer and DOM visibility. Its minimal host, private
WeakMap generations and closure checks prevent expired or reentrant writes from
overwriting newer messages. Public art/timer fields and destroy/show methods remain.
False/empty assignment only hides the notice; the old pending expiry still clears
its text. Manual notice.destroy cancels the timer without hiding or permanently
disabling a live instance. The source getter is boolean; the legacy public message
read type remains tracked by BASE-TYPE-07 for compatible coordination in CORE-21.
`src/plugins/fastForward.ts` wires the unchanged plugin result and input events.
`src/input/long-press.ts` owns one press at a time in the current source scope,
including its timer and previous playback rate. Touch move/end/cancel, lock, pause,
source disposal and player destruction release the press. The native media element
still performs its own defaultPlaybackRate reset on load/source replacement.
Resource cleanup must not create a second press or overwrite a newer reentrant one.
The typed builtin factory cast is confined to the existing JS constructor boundary;
CORE-20 must revisit it when the full constructor host becomes typed.
Run `test/notice.test.js` and `test/fast-forward.test.js` through the Node runner;
their matching `test/types` fixtures check source inference and retained consumers.
The matching browser specs compare frozen releases with candidate builds using
actual video playback and controlled DOM touch events. They do not certify physical
touch devices.
`src/plugins/autoPlayback.ts` keeps the resume plugin's name/times/clear/delete
surface. `auto-playback/records.ts` writes the existing times member of storage
using option.id or option.url. Keep its historical strict `length > max` pruning
and single-oldest deletion; changing the retention policy is a separate behavior
decision. Playback recording remains instance-owned when the prompt layer is removed.
`auto-playback/prompt.ts` prepares the original DOM before recording is subscribed,
then installs ready/restart handlers. Each prompt owns its click listeners, one
first-timeupdate subscription and timer under its layer, and is also cancelled by
source disposal. Restart keeps only the latest resume target. Close only hides the
prompt; seek/play precede poster/prompt hiding unless reentry invalidates that work.
Extend `test/auto-playback.test.js`, its type fixture and browser spec for storage,
restart, source/layer lifecycle, or actual resume playback changes.
`src/plugins/lock.ts` preserves class -> isLock -> lock event ordering, including
notifications for repeated assignments. Its icon subscription belongs to the lock
layer; removing that layer does not disable a live plugin's state API. Destruction
does disable further writes through retained setters/clicks. `miniProgressBar.ts`
keeps its name-only result and control-driven class, with an instance-owned
subscription. Both stay in one file because their responsibilities remain small.
`test/builtin-layers.test.js` and its browser/type fixtures cover these contracts
and the mobile/desktop, live/VOD builtin installation matrix. User-agent fixtures
exercise the branch conditions, not physical mobile capability certification.
`src/info.ts` owns a single initialization generation without adding public fields;
`src/info/poll.ts` reads the current initialization's panel/media targets, converts
media values as the native textContent setter does, and owns its timer and close
listener. Reinitializing replaces that work; mobile construction still waits for an
explicit init. Polling continues while the panel is hidden to preserve old behavior.
`src/loading.ts` retains its simple icon insertion and Component facade.
`src/mask.ts` keeps its original destroy listener position so earlier/later consumer
observers see the same icon states. The internal lifecycle finalization scope runs
after destroy event dispatch (even on error) and performs any missing terminal
display/cleanup. Direct root-scope disposal releases finalizers after root resources.
Use this phase only for work whose original event order must remain observable;
ordinary timers/listeners still belong in the root or feature scope.
`test/prompt-components.test.js` and its browser/type fixtures cover these modules,
including getter/DOM reentry, conversion, listener order and throwing observers.
## Keyboard input (CORE-17)
hotkey.ts retains the art/keys own fields, init/add/remove prototype methods, plain
@@ -546,7 +609,8 @@ minimal option hosts and internal sync/Promise return types. builtins.ts install
five existing builtins in their original order and reads each condition at its turn.
It keeps the mobile/live exclusions, including no fastForward for live media. The
constructor captures option once before builtin installation, then traverses the
same live user-plugin array. Builtin implementations remain JS until CORE-18.
same live user-plugin array. All five builtin implementations are now TypeScript;
their minimum-host factory casts remain confined to this constructor boundary until CORE-20.
registration.ts owns result naming and non-enumerable/non-writable/non-configurable
registry properties. Names still prefer result.name, then factory.name, then the
+1 -1
View File
@@ -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 n=(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 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 i(t,e,n),a(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),i(s,l,c),a(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),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(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 a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={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 n=(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 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 i(t,e,n),a(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),i(s,l,c),a(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),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(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 a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={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
View File
@@ -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,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=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={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.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 i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=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 i(t,e,n),a(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),i(s,l,c),a(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),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(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 a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?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=u(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,i=Object.prototype.hasOwnProperty,a=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={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.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 i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=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 i(t,e,n),a(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),i(s,l,c),a(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),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(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 a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?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=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+397 -143
View File
@@ -361,13 +361,18 @@ class ResourceScope {
throw new ResourceCleanupError(errors);
}
}
const states$4 = /* @__PURE__ */ new WeakMap();
const states$6 = /* @__PURE__ */ new WeakMap();
const containers = /* @__PURE__ */ new WeakMap();
function beginLifecycle(owner) {
states$4.set(owner, { scope: new ResourceScope(), destroying: false });
const state2 = { scope: new ResourceScope(), finalizers: new ResourceScope(), destroying: false };
states$6.set(owner, state2);
state2.scope.add(() => {
if (!state2.destroying)
state2.finalizers.dispose();
});
}
function stateOf$1(owner) {
const state2 = states$4.get(owner);
const state2 = states$6.get(owner);
if (!state2)
throw new Error("ArtPlayer lifecycle has not been initialized");
return state2;
@@ -375,6 +380,9 @@ function stateOf$1(owner) {
function getScope(owner) {
return stateOf$1(owner).scope;
}
function getFinalizationScope(owner) {
return stateOf$1(owner).finalizers;
}
function isClosing(owner) {
const state2 = stateOf$1(owner);
return state2.destroying || state2.scope.closed;
@@ -422,6 +430,7 @@ function destroyInstance(owner, instances2, removeHtml, removeSource, failed = f
state2.releaseContainer = void 0;
}
attempt(() => owner.emit("destroy"));
attempt(() => state2.finalizers.dispose());
if (failed && state2.rollback)
attempt(state2.rollback);
state2.rollback = void 0;
@@ -2536,12 +2545,12 @@ function hoverInit(art, events) {
}
);
}
const states$3 = /* @__PURE__ */ new WeakMap();
const states$5 = /* @__PURE__ */ new WeakMap();
function ownListeners(registry, art) {
states$3.set(registry, { scope: getScope(art), cleaning: false });
states$5.set(registry, { scope: getScope(art), cleaning: false });
}
function stateOf(registry) {
const state2 = states$3.get(registry);
const state2 = states$5.get(registry);
if (!state2)
throw new Error("ArtPlayer event registry has not been initialized");
return state2;
@@ -2823,12 +2832,12 @@ function acceptsHotkey(event, fallback) {
const first = asElement(event.composedPath?.()[0]) || asElement(event.target);
return !first || !editable(first);
}
const states$2 = /* @__PURE__ */ new WeakMap();
const states$4 = /* @__PURE__ */ new WeakMap();
class Hotkey {
constructor(art) {
this.art = art;
this.keys = {};
states$2.set(this, { subscribed: false });
states$4.set(this, { subscribed: false });
if (!isMobile)
this.init();
}
@@ -2836,7 +2845,7 @@ class Hotkey {
const art = this.art;
if (isClosing(art))
return;
const state2 = states$2.get(this);
const state2 = states$4.get(this);
if (art.option.hotkey) {
if (!state2.defaults) {
const callbacks = defaultHotkeys(art);
@@ -3028,40 +3037,85 @@ class Icons {
}
}
}
function pollInfo(art, scope, close2) {
const active2 = () => !scope.closed && !isClosing(art);
const fail = (error2) => {
try {
scope.dispose();
} catch (cleanupError) {
console.warn("Failed to release info polling resources:", cleanupError);
}
throw error2;
};
try {
const { proxy, constructor, template: { $infoPanel, $infoClose, $video } } = art;
const cleanup = proxy($infoClose, "click", () => {
if (active2())
close2();
});
scope.add(() => {
art.events.remove(cleanup);
});
const items = Array.from($infoPanel.querySelectorAll("[data-video]"));
const destroy = () => scope.dispose();
art.on("destroy", destroy);
scope.add(() => {
art.off("destroy", destroy);
});
const loop = () => {
try {
for (const item of items) {
if (!active2())
return;
const value = Reflect.get($video, item.dataset.video ?? "undefined");
if (!active2())
return;
const raw = typeof value === "number" ? value.toFixed(2) : value;
if (!active2())
return;
if (item.textContent !== raw) {
const text = raw == null ? null : `${raw}`;
if (!active2())
return;
item.textContent = text;
}
}
if (active2())
timeout(scope, loop, constructor.INFO_LOOP_TIME);
} catch (error2) {
fail(error2);
}
};
if (active2())
loop();
} catch (error2) {
fail(error2);
}
}
const states$3 = /* @__PURE__ */ new WeakMap();
class Info extends Component {
constructor(art) {
super(art);
this.name = "info";
if (!isMobile) {
states$3.set(this, { revision: 0 });
if (!isMobile)
this.init();
}
}
init() {
const {
proxy,
constructor,
template: { $infoPanel, $infoClose, $video }
} = this.art;
proxy($infoClose, "click", () => {
const state2 = states$3.get(this);
const revision = ++state2.revision;
state2.scope?.dispose();
if (isClosing(this.art) || state2.revision !== revision)
return;
const scope = getScope(this.art).child();
state2.scope = scope;
scope.add(() => {
if (state2.scope === scope)
state2.scope = void 0;
});
pollInfo(this.art, scope, () => {
this.show = false;
});
let cancel = () => {
};
const $types = queryAll("[data-video]", $infoPanel) || [];
this.art.on("destroy", () => cancel());
const scope = getScope(this.art);
function loop() {
for (let index = 0; index < $types.length; index++) {
const item = $types[index];
const value = $video[item.dataset.video];
const textContent = typeof value === "number" ? value.toFixed(2) : value;
if (item.textContent !== textContent) {
item.textContent = textContent;
}
}
cancel = timeout(scope, loop, constructor.INFO_LOOP_TIME);
}
loop();
}
}
class Layer extends Component {
@@ -3078,7 +3132,9 @@ class Loading extends Component {
constructor(art) {
super(art);
this.name = "loading";
append(art.template.$loading, art.icons.loading);
const icon = art.icons.loading;
if (!isClosing(art))
appendElement(art.template.$loading, icon);
}
}
class Mask extends Component {
@@ -3086,44 +3142,103 @@ class Mask extends Component {
super(art);
this.name = "mask";
const { template, icons, events } = art;
const $state = append(template.$state, icons.state);
const $error = append(template.$state, icons.error);
const stateIcon = icons.state;
if (isClosing(art))
return;
const $state = appendElement(template.$state, stateIcon);
const errorIcon = icons.error;
if (isClosing(art))
return;
const $error = appendElement(template.$state, errorIcon);
setStyle($error, "display", "none");
art.on("destroy", () => {
let terminalEventHandled = false;
const destroy = () => {
setStyle($state, "display", "none");
setStyle($error, "display", null);
if (isClosing(art))
terminalEventHandled = true;
};
art.on("destroy", destroy);
getFinalizationScope(art).add(() => {
try {
if (!terminalEventHandled)
destroy();
} finally {
art.off("destroy", destroy);
}
});
events.proxy(template.$state, "click", () => silencePromise(art.play()));
if (!isClosing(art)) {
events.proxy(template.$state, "click", () => {
if (!isClosing(art))
silencePromise(art.play());
});
}
}
}
const states$2 = /* @__PURE__ */ new WeakMap();
function cancelNotice(notice) {
const state2 = states$2.get(notice);
state2.revision++;
state2.pending = void 0;
if (notice.timer !== null) {
clearTimeout(notice.timer);
notice.timer = null;
}
}
class Notice {
constructor(art) {
this.art = art;
this.timer = null;
getScope(art).add(() => this.destroy());
art.on("destroy", () => this.destroy());
states$2.set(this, { revision: 0 });
const destroy = () => this.destroy();
art.on("destroy", destroy);
getScope(art).add(() => {
art.off("destroy", destroy);
});
getScope(art).add(() => {
this.destroy();
});
}
destroy() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
cancelNotice(this);
}
set show(msg) {
if (getScope(this.art).closed)
if (isClosing(this.art))
return;
const state2 = states$2.get(this);
state2.revision++;
const {
constructor,
template: { $player, $noticeInner }
} = this.art;
if (msg) {
cancelNotice(this);
const revision = state2.revision;
const active2 = () => !isClosing(this.art) && state2.revision === revision;
$noticeInner.textContent = msg instanceof Error ? msg.message.trim() : msg;
if (!active2())
return;
addClass($player, "art-notice-show");
clearTimeout(this.timer);
this.timer = setTimeout(() => {
if (!active2())
return;
const delay = constructor.NOTICE_TIME;
if (!active2())
return;
const pending2 = {};
state2.pending = pending2;
const timer = setTimeout(() => {
if (isClosing(this.art) || state2.pending !== pending2)
return;
state2.pending = void 0;
const before = state2.revision;
$noticeInner.textContent = "";
removeClass($player, "art-notice-show");
}, constructor.NOTICE_TIME);
if (!isClosing(this.art) && state2.revision === before)
removeClass($player, "art-notice-show");
}, delay);
if (active2())
this.timer = timer;
else
clearTimeout(timer);
} else {
removeClass($player, "art-notice-show");
}
@@ -5451,16 +5566,31 @@ function autoOrientation(art) {
}
};
}
function autoPlayback(art) {
const {
i18n,
icons,
storage,
constructor,
proxy,
template: { $poster }
} = art;
const $autoPlayback = art.layers.add({
function readTimes(storage) {
return storage.get("times") || {};
}
function installPlaybackRecords(art) {
const { storage, constructor } = art;
eventSubscriptions(art)("video:timeupdate", () => {
if (!art.playing)
return;
const times = readTimes(storage);
const keys2 = Object.keys(times);
const max = constructor.AUTO_PLAYBACK_MAX;
const key = art.option.id || art.option.url;
const time2 = art.currentTime;
if (isClosing(art))
return;
if (keys2.length > max)
delete times[keys2[0]];
times[key] = time2;
if (!isClosing(art))
storage.set("times", times);
});
}
function installResumePrompt(art) {
const { i18n, icons, storage, constructor, template: { $poster } } = art;
const element = art.layers.add({
name: "auto-playback",
html: `
<div class="art-auto-playback-close"></div>
@@ -5468,95 +5598,204 @@ function autoPlayback(art) {
<div class="art-auto-playback-jump"></div>
`
});
const $last = query(".art-auto-playback-last", $autoPlayback);
const $jump = query(".art-auto-playback-jump", $autoPlayback);
const $close = query(".art-auto-playback-close", $autoPlayback);
append($close, icons.close);
let timer = null;
art.on("video:timeupdate", () => {
if (art.playing) {
const times = storage.get("times") || {};
const keys2 = Object.keys(times);
if (keys2.length > constructor.AUTO_PLAYBACK_MAX) {
delete times[keys2[0]];
if (!element || isClosing(art))
return () => {
};
const $last = queryElement(".art-auto-playback-last", element);
const $jump = queryElement(".art-auto-playback-jump", element);
const $close = queryElement(".art-auto-playback-close", element);
appendElement($close, icons.close);
const owner = entryScope(element);
let current2;
let revision = 0;
const init = () => {
const generation = ++revision;
current2?.dispose();
if (owner.closed || isClosing(art) || revision !== generation)
return;
const scope = owner.child();
current2 = scope;
const active2 = () => current2 === scope && !scope.closed && !isClosing(art);
scope.add(() => {
if (current2 === scope) {
current2 = void 0;
setStyle(element, "display", "none");
}
times[art.option.id || art.option.url] = art.currentTime;
storage.set("times", times);
}
});
function init() {
const times = storage.get("times") || {};
});
const releaseSource = getSourceScope(art).add(() => {
scope.dispose();
});
scope.add(() => {
releaseSource();
});
const times = readTimes(storage);
const currentTime = times[art.option.id || art.option.url];
clearTimeout(timer);
setStyle($autoPlayback, "display", "none");
if (currentTime && currentTime >= constructor.AUTO_PLAYBACK_MIN) {
setStyle($autoPlayback, "display", "flex");
$last.textContent = `${i18n.get("Last Seen")} ${secondToTime(currentTime)}`;
$jump.textContent = i18n.get("Jump Play");
proxy($close, "click", () => {
setStyle($autoPlayback, "display", "none");
if (!active2())
return;
setStyle(element, "display", "none");
if (!active2() || !currentTime || !(currentTime >= constructor.AUTO_PLAYBACK_MIN))
return;
if (!active2())
return;
setStyle(element, "display", "flex");
if (!active2())
return;
const lastText = `${i18n.get("Last Seen")} ${secondToTime(currentTime)}`;
if (!active2())
return;
$last.textContent = lastText;
if (!active2())
return;
const jumpText = i18n.get("Jump Play");
if (!active2())
return;
$jump.textContent = jumpText;
if (!active2())
return;
const bind2 = (target, callback) => {
const cleanup = art.events.proxy(target, "click", () => {
if (active2())
callback();
});
proxy($jump, "click", () => {
art.seek = currentTime;
silencePromise(art.play());
setStyle($poster, "display", "none");
setStyle($autoPlayback, "display", "none");
scope.add(() => {
art.events.remove(cleanup);
});
art.once("video:timeupdate", () => {
timer = setTimeout(() => {
setStyle($autoPlayback, "display", "none");
}, constructor.AUTO_PLAYBACK_TIMEOUT);
});
}
}
art.on("ready", init);
art.on("restart", init);
};
bind2($close, () => {
setStyle(element, "display", "none");
});
if (!active2())
return;
bind2($jump, () => {
art.seek = currentTime;
if (!active2())
return;
silencePromise(art.play());
if (!active2())
return;
setStyle($poster, "display", "none");
if (active2())
setStyle(element, "display", "none");
});
if (!active2())
return;
let releaseUpdate = () => {
};
let fired = false;
const update = () => {
if (fired || !active2())
return;
fired = true;
releaseUpdate();
timeout(scope, () => {
if (active2())
setStyle(element, "display", "none");
}, constructor.AUTO_PLAYBACK_TIMEOUT);
};
art.on("video:timeupdate", update);
releaseUpdate = scope.add(() => {
art.off("video:timeupdate", update);
});
if (fired)
releaseUpdate();
};
return () => {
subscribeEntry(art, element, "ready", init);
subscribeEntry(art, element, "restart", init);
};
}
function autoPlayback(art) {
const { storage } = art;
const installPrompt = installResumePrompt(art);
installPlaybackRecords(art);
installPrompt();
return {
name: "auto-playback",
get times() {
return storage.get("times") || {};
return readTimes(storage);
},
clear() {
return storage.del("times");
},
delete(id2) {
const times = storage.get("times") || {};
const times = readTimes(storage);
delete times[id2];
storage.set("times", times);
return times;
}
};
}
function longPress(art) {
let current2;
let generation = 0;
const hasActivePress = () => current2?.active;
const stop = () => {
generation++;
current2?.scope.dispose();
};
const start = (event) => {
const revision = ++generation;
current2?.scope.dispose();
if (revision !== generation || isClosing(art) || event.touches.length !== 1 || !art.playing || art.isLock)
return;
const press = { scope: getSourceScope(art).child(), active: false, previousRate: 1 };
current2 = press;
press.scope.add(() => {
if (current2 !== press)
return;
current2 = void 0;
if (press.active) {
try {
art.playbackRate = press.previousRate;
} finally {
if (!hasActivePress())
removeClass(art.template.$player, "art-fast-forward");
}
}
});
timeout(press.scope, () => {
if (current2 !== press || isClosing(art) || !art.playing || art.isLock) {
press.scope.dispose();
return;
}
try {
press.previousRate = art.playbackRate;
if (current2 !== press || press.scope.closed)
return;
press.active = true;
const rate = art.constructor.FAST_FORWARD_VALUE;
if (current2 !== press || press.scope.closed || isClosing(art))
return;
art.playbackRate = rate;
if (current2 === press && !press.scope.closed && !isClosing(art))
addClass(art.template.$player, "art-fast-forward");
} catch (error2) {
try {
press.scope.dispose();
} catch (cleanupError) {
console.warn("Failed to restore fast-forward playback rate:", cleanupError);
}
throw error2;
}
}, art.constructor.FAST_FORWARD_TIME);
};
return { start, stop };
}
function fastForward(art) {
const {
constructor,
proxy,
template: { $player, $video }
} = art;
let timer = null;
let isPress = false;
let lastPlaybackRate = 1;
const onStart = (event) => {
if (event.touches.length === 1 && art.playing && !art.isLock) {
timer = setTimeout(() => {
isPress = true;
lastPlaybackRate = art.playbackRate;
art.playbackRate = constructor.FAST_FORWARD_VALUE;
addClass($player, "art-fast-forward");
}, constructor.FAST_FORWARD_TIME);
}
};
const onStop = () => {
clearTimeout(timer);
if (isPress) {
isPress = false;
art.playbackRate = lastPlaybackRate;
removeClass($player, "art-fast-forward");
}
};
proxy($video, "touchstart", onStart);
art.on("document:touchmove", onStop);
art.on("document:touchend", onStop);
const { proxy, template: { $player, $video } } = art;
const press = longPress(art);
const subscribe = eventSubscriptions(art);
proxy($video, "touchstart", (event) => press.start(event));
proxy($video, "touchcancel", press.stop);
subscribe("document:touchmove", press.stop);
subscribe("document:touchend", press.stop);
subscribe("document:touchcancel", press.stop);
subscribe("video:pause", press.stop);
subscribe("destroy", press.stop);
subscribe("lock", (locked) => {
if (locked)
press.stop();
});
return {
name: "fastForward",
get state() {
@@ -5574,29 +5813,44 @@ function lock(art) {
return hasClass($player, "art-lock");
}
function setLock() {
if (isClosing(art))
return;
addClass($player, "art-lock");
if (isClosing(art))
return;
art.isLock = true;
art.emit("lock", true);
if (!isClosing(art))
art.emit("lock", true);
}
function setUnlock() {
if (isClosing(art))
return;
removeClass($player, "art-lock");
if (isClosing(art))
return;
art.isLock = false;
art.emit("lock", false);
if (!isClosing(art))
art.emit("lock", false);
}
layers.add({
name: "lock",
mounted($el) {
const $lock = append($el, icons.lock);
const $unlock = append($el, icons.unlock);
const scope = entryScope($el);
if (scope.closed || isClosing(art))
return;
const $lock = appendElement($el, icons.lock);
if (scope.closed || isClosing(art))
return;
const $unlock = appendElement($el, icons.unlock);
if (scope.closed || isClosing(art))
return;
setStyle($lock, "display", "none");
art.on("lock", (state2) => {
if (state2) {
setStyle($lock, "display", "inline-flex");
setStyle($unlock, "display", "none");
} else {
setStyle($lock, "display", "none");
setStyle($unlock, "display", "inline-flex");
}
subscribeEntry(art, $el, "lock", (state2) => {
if (isClosing(art))
return;
setStyle($lock, "display", state2 ? "inline-flex" : "none");
if (!scope.closed && !isClosing(art))
setStyle($unlock, "display", state2 ? "none" : "inline-flex");
});
},
click() {
@@ -5622,7 +5876,7 @@ function lock(art) {
};
}
function miniProgressBar(art) {
art.on("control", (state2) => {
eventSubscriptions(art)("control", (state2) => {
if (state2) {
removeClass(art.template.$player, "art-mini-progress-bar");
} else {
-46
View File
@@ -1,46 +0,0 @@
import { getScope } from './lifecycle/instance'
import { timeout } from './lifecycle/resources'
import { isMobile, queryAll } from './utils'
import Component from './utils/component'
export default class Info extends Component {
constructor(art) {
super(art)
this.name = 'info'
if (!isMobile) {
this.init()
}
}
init() {
const {
proxy,
constructor,
template: { $infoPanel, $infoClose, $video },
} = this.art
proxy($infoClose, 'click', () => {
this.show = false
})
let cancel = () => {}
const $types = queryAll('[data-video]', $infoPanel) || []
this.art.on('destroy', () => cancel())
const scope = getScope(this.art)
function loop() {
for (let index = 0; index < $types.length; index++) {
const item = $types[index]
const value = $video[item.dataset.video]
const textContent = typeof value === 'number' ? value.toFixed(2) : value
if (item.textContent !== textContent) {
item.textContent = textContent
}
}
cancel = timeout(scope, loop, constructor.INFO_LOOP_TIME)
}
loop()
}
}
+35
View File
@@ -0,0 +1,35 @@
import type { InfoHost } from './info/poll'
import type ResourceScope from './lifecycle/scope'
import { pollInfo } from './info/poll'
import { getScope, isClosing } from './lifecycle/instance'
import { isMobile } from './utils'
import Component from './utils/component'
const states = new WeakMap<Info, { scope?: ResourceScope, revision: number }>()
export default class Info extends Component<InfoHost> {
constructor(art: InfoHost) {
super(art)
this.name = 'info'
states.set(this, { revision: 0 })
if (!isMobile)
this.init()
}
init(): void {
const state = states.get(this)!
const revision = ++state.revision
state.scope?.dispose()
if (isClosing(this.art) || state.revision !== revision)
return
const scope = getScope(this.art).child()
state.scope = scope
scope.add(() => {
if (state.scope === scope)
state.scope = undefined
})
pollInfo(this.art, scope, () => {
this.show = false
})
}
}
+71
View File
@@ -0,0 +1,71 @@
import type { SubscriptionHost } from '../component/resources'
import type { ComponentHost } from '../component/types'
import type ResourceScope from '../lifecycle/scope'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
export interface InfoHost extends ComponentHost, SubscriptionHost<{ destroy: [] }> {
template: { $player: HTMLElement, $infoPanel: HTMLElement, $infoClose: HTMLElement, $video: object }
constructor: { INFO_LOOP_TIME: number }
proxy: ComponentHost['events']['proxy']
}
export function pollInfo(art: InfoHost, scope: ResourceScope, close: () => void): void {
const active = () => !scope.closed && !isClosing(art)
const fail = (error: unknown): never => {
try {
scope.dispose()
}
catch (cleanupError) {
console.warn('Failed to release info polling resources:', cleanupError)
}
throw error
}
try {
const { proxy, constructor, template: { $infoPanel, $infoClose, $video } } = art
const cleanup = proxy($infoClose, 'click', () => {
if (active())
close()
})
scope.add(() => {
art.events.remove(cleanup)
})
const items = Array.from($infoPanel.querySelectorAll<HTMLElement>('[data-video]'))
const destroy = () => scope.dispose()
art.on('destroy', destroy)
scope.add(() => {
art.off('destroy', destroy)
})
const loop = () => {
try {
for (const item of items) {
if (!active())
return
const value: unknown = Reflect.get($video, item.dataset.video ?? 'undefined')
if (!active())
return
const raw = typeof value === 'number' ? value.toFixed(2) : value
if (!active())
return
if (item.textContent !== raw) {
// Match the DOM nullable-string conversion, including Symbol errors.
const text = raw == null ? null : `${raw}`
if (!active())
return
item.textContent = text
}
}
if (active())
timeout(scope, loop, constructor.INFO_LOOP_TIME)
}
catch (error) {
fail(error)
}
}
if (active())
loop()
}
catch (error) {
fail(error)
}
}
@@ -0,0 +1,80 @@
import type ResourceScope from '../lifecycle/scope'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
import { getSourceScope } from '../source/operation'
import { addClass, removeClass } from '../utils'
export interface LongPressHost {
template: { $player: HTMLElement }
constructor: { FAST_FORWARD_TIME: number, FAST_FORWARD_VALUE: number }
playing: boolean
isLock: boolean
playbackRate: number
}
interface Press {
scope: ResourceScope
active: boolean
previousRate: number
}
export function longPress(art: LongPressHost) {
let current: Press | undefined
let generation = 0
const hasActivePress = () => current?.active
const stop = () => {
generation++
current?.scope.dispose()
}
const start = (event: TouchEvent) => {
const revision = ++generation
current?.scope.dispose()
if (revision !== generation || isClosing(art) || event.touches.length !== 1 || !art.playing || art.isLock)
return
const press: Press = { scope: getSourceScope(art).child(), active: false, previousRate: 1 }
current = press
press.scope.add(() => {
if (current !== press)
return
current = undefined
if (press.active) {
try {
art.playbackRate = press.previousRate
}
finally {
// A callback may have already started and activated a newer press.
if (!hasActivePress())
removeClass(art.template.$player, 'art-fast-forward')
}
}
})
timeout(press.scope, () => {
if (current !== press || isClosing(art) || !art.playing || art.isLock) {
press.scope.dispose()
return
}
try {
press.previousRate = art.playbackRate
if (current !== press || press.scope.closed)
return
press.active = true
const rate = art.constructor.FAST_FORWARD_VALUE
if (current !== press || press.scope.closed || isClosing(art))
return
art.playbackRate = rate
if (current === press && !press.scope.closed && !isClosing(art))
addClass(art.template.$player, 'art-fast-forward')
}
catch (error) {
try {
press.scope.dispose()
}
catch (cleanupError) {
console.warn('Failed to restore fast-forward playback rate:', cleanupError)
}
throw error
}
}, art.constructor.FAST_FORWARD_TIME)
}
return { start, stop }
}
+13 -1
View File
@@ -9,6 +9,7 @@ interface LifecycleOwner {
interface State {
scope: ResourceScope
finalizers: ResourceScope
destroying: boolean
rollback?: () => void
releaseContainer?: () => void
@@ -18,7 +19,13 @@ const states = new WeakMap<object, State>()
const containers = new WeakMap<Element, object>()
export function beginLifecycle(owner: object): void {
states.set(owner, { scope: new ResourceScope(), destroying: false })
const state: State = { scope: new ResourceScope(), finalizers: new ResourceScope(), destroying: false }
states.set(owner, state)
state.scope.add(() => {
// Direct scope disposal has no destroy-event phase to wait for.
if (!state.destroying)
state.finalizers.dispose()
})
}
function stateOf(owner: object): State {
@@ -32,6 +39,10 @@ export function getScope(owner: object): ResourceScope {
return stateOf(owner).scope
}
export function getFinalizationScope(owner: object): ResourceScope {
return stateOf(owner).finalizers
}
export function isClosing(owner: object): boolean {
const state = stateOf(owner)
return state.destroying || state.scope.closed
@@ -83,6 +94,7 @@ export function destroyInstance(owner: LifecycleOwner, instances: LifecycleOwner
state.releaseContainer = undefined
}
attempt(() => owner.emit('destroy'))
attempt(() => state.finalizers.dispose())
if (failed && state.rollback)
attempt(state.rollback)
state.rollback = undefined
-10
View File
@@ -1,10 +0,0 @@
import { append } from './utils'
import Component from './utils/component'
export default class Loading extends Component {
constructor(art) {
super(art)
this.name = 'loading'
append(art.template.$loading, art.icons.loading)
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { ComponentHost } from './component/types'
import { appendElement } from './component/dom'
import { isClosing } from './lifecycle/instance'
import Component from './utils/component'
export interface LoadingHost extends ComponentHost {
template: { $player: HTMLElement, $loading: HTMLElement }
icons: { loading: string | Element }
}
export default class Loading extends Component<LoadingHost> {
constructor(art: LoadingHost) {
super(art)
this.name = 'loading'
const icon = art.icons.loading
if (!isClosing(art))
appendElement(art.template.$loading, icon)
}
}
-23
View File
@@ -1,23 +0,0 @@
import { append, setStyle, silencePromise } from './utils'
import Component from './utils/component'
export default class Mask extends Component {
constructor(art) {
super(art)
this.name = 'mask'
const { template, icons, events } = art
const $state = append(template.$state, icons.state)
const $error = append(template.$state, icons.error)
setStyle($error, 'display', 'none')
art.on('destroy', () => {
setStyle($state, 'display', 'none')
setStyle($error, 'display', null)
})
events.proxy(template.$state, 'click', () => silencePromise(art.play()))
}
}
+57
View File
@@ -0,0 +1,57 @@
import type { SubscriptionHost } from './component/resources'
import type { ComponentHost } from './component/types'
import { appendElement } from './component/dom'
import { getFinalizationScope, isClosing } from './lifecycle/instance'
import { setStyle, silencePromise } from './utils'
import Component from './utils/component'
export interface MaskHost extends ComponentHost, SubscriptionHost<{ destroy: [] }> {
template: { $player: HTMLElement, $state: HTMLElement }
icons: { state: string | Element, error: string | Element }
play: () => unknown
}
export default class Mask extends Component<MaskHost> {
constructor(art: MaskHost) {
super(art)
this.name = 'mask'
const { template, icons, events } = art
const stateIcon = icons.state
if (isClosing(art))
return
const $state = appendElement(template.$state, stateIcon)
const errorIcon = icons.error
if (isClosing(art))
return
const $error = appendElement(template.$state, errorIcon)
setStyle($error, 'display', 'none')
let terminalEventHandled = false
const destroy = () => {
setStyle($state, 'display', 'none')
setStyle($error, 'display', null)
if (isClosing(art))
terminalEventHandled = true
}
art.on('destroy', destroy)
getFinalizationScope(art).add(() => {
try {
if (!terminalEventHandled)
destroy()
}
finally {
art.off('destroy', destroy)
}
})
if (!isClosing(art)) {
events.proxy(template.$state, 'click', () => {
if (!isClosing(art))
silencePromise(art.play())
})
}
}
}
-48
View File
@@ -1,48 +0,0 @@
import { getScope } from './lifecycle/instance'
import { addClass, removeClass } from './utils'
export default class Notice {
constructor(art) {
this.art = art
this.timer = null
getScope(art).add(() => this.destroy())
art.on('destroy', () => this.destroy())
}
destroy() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
}
set show(msg) {
if (getScope(this.art).closed)
return
const {
constructor,
template: { $player, $noticeInner },
} = this.art
if (msg) {
$noticeInner.textContent = msg instanceof Error ? msg.message.trim() : msg
addClass($player, 'art-notice-show')
clearTimeout(this.timer)
this.timer = setTimeout(() => {
$noticeInner.textContent = ''
removeClass($player, 'art-notice-show')
}, constructor.NOTICE_TIME)
}
else {
removeClass($player, 'art-notice-show')
}
}
get show() {
const {
template: { $player },
} = this.art
return $player.classList.contains('art-notice-show')
}
}
+99
View File
@@ -0,0 +1,99 @@
import type { SubscriptionHost } from './component/resources'
import { getScope, isClosing } from './lifecycle/instance'
import { addClass, removeClass } from './utils'
interface NoticeHost extends SubscriptionHost<{ destroy: [] }> {
constructor: { NOTICE_TIME: number }
template: { $player: HTMLElement, $noticeInner: HTMLElement }
}
interface NoticeState {
revision: number
pending?: object
}
const states = new WeakMap<Notice, NoticeState>()
function cancelNotice(notice: Notice): void {
const state = states.get(notice)!
state.revision++
state.pending = undefined
if (notice.timer !== null) {
clearTimeout(notice.timer)
notice.timer = null
}
}
export default class Notice {
declare art: NoticeHost
declare timer: ReturnType<typeof setTimeout> | null
constructor(art: NoticeHost) {
this.art = art
this.timer = null
states.set(this, { revision: 0 })
const destroy = () => this.destroy()
art.on('destroy', destroy)
getScope(art).add(() => {
art.off('destroy', destroy)
})
getScope(art).add(() => {
this.destroy()
})
}
destroy(): void {
cancelNotice(this)
}
set show(msg: string | Error | false) {
if (isClosing(this.art))
return
const state = states.get(this)!
state.revision++
const {
constructor,
template: { $player, $noticeInner },
} = this.art
if (msg) {
cancelNotice(this)
const revision = state.revision
const active = () => !isClosing(this.art) && state.revision === revision
$noticeInner.textContent = msg instanceof Error ? msg.message.trim() : msg
if (!active())
return
addClass($player, 'art-notice-show')
if (!active())
return
const delay = constructor.NOTICE_TIME
if (!active())
return
const pending = {}
state.pending = pending
const timer = setTimeout(() => {
if (isClosing(this.art) || state.pending !== pending)
return
state.pending = undefined
const before = state.revision
$noticeInner.textContent = ''
if (!isClosing(this.art) && state.revision === before)
removeClass($player, 'art-notice-show')
}, delay)
if (active())
this.timer = timer
else
clearTimeout(timer)
}
else {
removeClass($player, 'art-notice-show')
}
}
get show(): boolean {
const {
template: { $player },
} = this.art
return $player.classList.contains('art-notice-show')
}
}
@@ -0,0 +1,144 @@
import type { SubscriptionHost } from '../../component/resources'
import type { ComponentHost } from '../../component/types'
import type ResourceScope from '../../lifecycle/scope'
import type { PlaybackStorage } from './records'
import { appendElement, queryElement } from '../../component/dom'
import { entryScope, subscribeEntry } from '../../component/resources'
import { isClosing } from '../../lifecycle/instance'
import { timeout } from '../../lifecycle/resources'
import { getSourceScope } from '../../source/operation'
import { secondToTime, setStyle, silencePromise } from '../../utils'
import { readTimes } from './records'
interface PromptEvents {
'ready': []
'restart': [string]
'video:timeupdate': [Event]
}
export interface ResumePromptHost extends ComponentHost, SubscriptionHost<PromptEvents> {
template: { $player: HTMLElement, $poster: HTMLElement }
constructor: { AUTO_PLAYBACK_MIN: number, AUTO_PLAYBACK_TIMEOUT: number }
layers: { add: (option: { name: string, html: string }) => HTMLDivElement | undefined }
icons: { close: string | Element }
i18n: { get: (key: string) => string }
storage: PlaybackStorage
option: { id?: string, url: string }
seek: number
play: () => unknown
}
export function installResumePrompt(art: ResumePromptHost): () => void {
const { i18n, icons, storage, constructor, template: { $poster } } = art
const element = art.layers.add({
name: 'auto-playback',
html: `
<div class="art-auto-playback-close"></div>
<div class="art-auto-playback-last"></div>
<div class="art-auto-playback-jump"></div>
`,
})
if (!element || isClosing(art))
return () => {}
const $last = queryElement('.art-auto-playback-last', element)
const $jump = queryElement('.art-auto-playback-jump', element)
const $close = queryElement('.art-auto-playback-close', element)
appendElement($close, icons.close)
const owner = entryScope(element)
let current: ResourceScope | undefined
let revision = 0
const init = () => {
const generation = ++revision
current?.dispose()
if (owner.closed || isClosing(art) || revision !== generation)
return
const scope = owner.child()
current = scope
const active = () => current === scope && !scope.closed && !isClosing(art)
scope.add(() => {
if (current === scope) {
current = undefined
setStyle(element, 'display', 'none')
}
})
const releaseSource = getSourceScope(art).add(() => {
scope.dispose()
})
scope.add(() => {
releaseSource()
})
const times = readTimes(storage)
const currentTime = times[art.option.id || art.option.url]
if (!active())
return
setStyle(element, 'display', 'none')
if (!active() || !currentTime || !(currentTime >= constructor.AUTO_PLAYBACK_MIN))
return
if (!active())
return
setStyle(element, 'display', 'flex')
if (!active())
return
const lastText = `${i18n.get('Last Seen')} ${secondToTime(currentTime)}`
if (!active())
return
$last.textContent = lastText
if (!active())
return
const jumpText = i18n.get('Jump Play')
if (!active())
return
$jump.textContent = jumpText
if (!active())
return
const bind = (target: HTMLElement, callback: () => void) => {
const cleanup = art.events.proxy(target, 'click', () => {
if (active())
callback()
})
scope.add(() => {
art.events.remove(cleanup)
})
}
bind($close, () => {
setStyle(element, 'display', 'none')
})
if (!active())
return
bind($jump, () => {
art.seek = currentTime
if (!active())
return
silencePromise(art.play())
if (!active())
return
setStyle($poster, 'display', 'none')
if (active())
setStyle(element, 'display', 'none')
})
if (!active())
return
let releaseUpdate = () => {}
let fired = false
const update = () => {
if (fired || !active())
return
fired = true
releaseUpdate()
timeout(scope, () => {
if (active())
setStyle(element, 'display', 'none')
}, constructor.AUTO_PLAYBACK_TIMEOUT)
}
art.on('video:timeupdate', update)
releaseUpdate = scope.add(() => {
art.off('video:timeupdate', update)
})
if (fired)
releaseUpdate()
}
return () => {
subscribeEntry<PromptEvents, 'ready'>(art, element, 'ready', init)
subscribeEntry<PromptEvents, 'restart'>(art, element, 'restart', init)
}
}
@@ -0,0 +1,44 @@
import type { SubscriptionHost } from '../../component/resources'
import { eventSubscriptions } from '../../events/subscriptions'
import { isClosing } from '../../lifecycle/instance'
export type PlaybackTimes = Record<string, number>
export interface PlaybackStorage {
get: (key: 'times') => PlaybackTimes | null | undefined
set: (key: 'times', value: PlaybackTimes) => void
del: (key: 'times') => void
}
export interface PlaybackRecordHost extends SubscriptionHost<{ 'video:timeupdate': [Event] }> {
storage: PlaybackStorage
constructor: { AUTO_PLAYBACK_MAX: number }
option: { id?: string, url: string }
playing: boolean
currentTime: number
}
export function readTimes(storage: PlaybackStorage): PlaybackTimes {
return storage.get('times') || {}
}
export function installPlaybackRecords(art: PlaybackRecordHost): void {
const { storage, constructor } = art
eventSubscriptions<{ 'video:timeupdate': [Event] }>(art)('video:timeupdate', () => {
if (!art.playing)
return
const times = readTimes(storage)
const keys = Object.keys(times)
const max = constructor.AUTO_PLAYBACK_MAX
const key = art.option.id || art.option.url
const time = art.currentTime
if (isClosing(art))
return
// Preserve the historical strict threshold and one-oldest-entry pruning.
if (keys.length > max)
delete times[keys[0]!]
times[key] = time
if (!isClosing(art))
storage.set('times', times)
})
}
@@ -1,91 +0,0 @@
import { append, query, secondToTime, setStyle, silencePromise } from '../utils'
export default function autoPlayback(art) {
const {
i18n,
icons,
storage,
constructor,
proxy,
template: { $poster },
} = art
const $autoPlayback = art.layers.add({
name: 'auto-playback',
html: `
<div class="art-auto-playback-close"></div>
<div class="art-auto-playback-last"></div>
<div class="art-auto-playback-jump"></div>
`,
})
const $last = query('.art-auto-playback-last', $autoPlayback)
const $jump = query('.art-auto-playback-jump', $autoPlayback)
const $close = query('.art-auto-playback-close', $autoPlayback)
append($close, icons.close)
let timer = null
art.on('video:timeupdate', () => {
if (art.playing) {
const times = storage.get('times') || {}
const keys = Object.keys(times)
if (keys.length > constructor.AUTO_PLAYBACK_MAX) {
delete times[keys[0]]
}
times[art.option.id || art.option.url] = art.currentTime
storage.set('times', times)
}
})
function init() {
const times = storage.get('times') || {}
const currentTime = times[art.option.id || art.option.url]
clearTimeout(timer)
setStyle($autoPlayback, 'display', 'none')
if (currentTime && currentTime >= constructor.AUTO_PLAYBACK_MIN) {
setStyle($autoPlayback, 'display', 'flex')
$last.textContent = `${i18n.get('Last Seen')} ${secondToTime(currentTime)}`
$jump.textContent = i18n.get('Jump Play')
proxy($close, 'click', () => {
setStyle($autoPlayback, 'display', 'none')
})
proxy($jump, 'click', () => {
art.seek = currentTime
silencePromise(art.play())
setStyle($poster, 'display', 'none')
setStyle($autoPlayback, 'display', 'none')
})
art.once('video:timeupdate', () => {
timer = setTimeout(() => {
setStyle($autoPlayback, 'display', 'none')
}, constructor.AUTO_PLAYBACK_TIMEOUT)
})
}
}
art.on('ready', init)
art.on('restart', init)
return {
name: 'auto-playback',
get times() {
return storage.get('times') || {}
},
clear() {
return storage.del('times')
},
delete(id) {
const times = storage.get('times') || {}
delete times[id]
storage.set('times', times)
return times
},
}
}
@@ -0,0 +1,29 @@
import type { ResumePromptHost } from './auto-playback/prompt'
import type { PlaybackRecordHost } from './auto-playback/records'
import { installResumePrompt } from './auto-playback/prompt'
import { installPlaybackRecords, readTimes } from './auto-playback/records'
export type AutoPlaybackHost = ResumePromptHost & PlaybackRecordHost
export default function autoPlayback(art: AutoPlaybackHost) {
const { storage } = art
// Keep time recording before the prompt's first-update timer subscription.
const installPrompt = installResumePrompt(art)
installPlaybackRecords(art)
installPrompt()
return {
name: 'auto-playback',
get times() {
return readTimes(storage)
},
clear() {
return storage.del('times')
},
delete(id: string) {
const times = readTimes(storage)
delete times[id]
storage.set('times', times)
return times
},
}
}
+4 -4
View File
@@ -10,15 +10,15 @@ import miniProgressBar from './miniProgressBar'
export default function installBuiltins<Host extends PluginHost<Host>>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
// Read each condition at its original turn; earlier plugins may change options.
if (!isClosing(registry.art) && option.miniProgressBar && !option.isLive)
registry.add(miniProgressBar)
registry.add(miniProgressBar as unknown as PluginFactory<Host>)
if (!isClosing(registry.art) && option.lock && isMobile)
registry.add(lock)
registry.add(lock as unknown as PluginFactory<Host>)
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback)
registry.add(autoPlayback as unknown as PluginFactory<Host>)
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
// The JS constructor supplies the complete host before builtin installation.
registry.add(autoOrientation as unknown as PluginFactory<Host>)
}
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward)
registry.add(fastForward as unknown as PluginFactory<Host>)
}
@@ -1,44 +0,0 @@
import { addClass, hasClass, removeClass } from '../utils'
export default function fastForward(art) {
const {
constructor,
proxy,
template: { $player, $video },
} = art
let timer = null
let isPress = false
let lastPlaybackRate = 1
const onStart = (event) => {
if (event.touches.length === 1 && art.playing && !art.isLock) {
timer = setTimeout(() => {
isPress = true
lastPlaybackRate = art.playbackRate
art.playbackRate = constructor.FAST_FORWARD_VALUE
addClass($player, 'art-fast-forward')
}, constructor.FAST_FORWARD_TIME)
}
}
const onStop = () => {
clearTimeout(timer)
if (isPress) {
isPress = false
art.playbackRate = lastPlaybackRate
removeClass($player, 'art-fast-forward')
}
}
proxy($video, 'touchstart', onStart)
art.on('document:touchmove', onStop)
art.on('document:touchend', onStop)
return {
name: 'fastForward',
get state() {
return hasClass($player, 'art-fast-forward')
},
}
}
@@ -0,0 +1,43 @@
import type { SubscriptionHost } from '../component/resources'
import type { LongPressHost } from '../input/long-press'
import type { PointerRegistry } from '../input/pointer-types'
import { eventSubscriptions } from '../events/subscriptions'
import { longPress } from '../input/long-press'
import { hasClass } from '../utils'
interface PressEvents {
'document:touchmove': [Event]
'document:touchend': [Event]
'document:touchcancel': [Event]
'video:pause': [Event]
'lock': [boolean]
'destroy': []
}
export interface FastForwardHost extends LongPressHost, SubscriptionHost<PressEvents> {
template: { $player: HTMLElement, $video: EventTarget }
proxy: PointerRegistry['proxy']
}
export default function fastForward(art: FastForwardHost): { name: string, readonly state: boolean } {
const { proxy, template: { $player, $video } } = art
const press = longPress(art)
const subscribe = eventSubscriptions<PressEvents>(art)
proxy($video, 'touchstart', event => press.start(event as TouchEvent))
proxy($video, 'touchcancel', press.stop)
subscribe('document:touchmove', press.stop)
subscribe('document:touchend', press.stop)
subscribe('document:touchcancel', press.stop)
subscribe('video:pause', press.stop)
subscribe('destroy', press.stop)
subscribe('lock', (locked) => {
if (locked)
press.stop()
})
return {
name: 'fastForward',
get state() {
return hasClass($player, 'art-fast-forward')
},
}
}
-68
View File
@@ -1,68 +0,0 @@
import { addClass, append, hasClass, removeClass, setStyle } from '../utils'
export default function lock(art) {
const {
layers,
icons,
template: { $player },
} = art
function getState() {
return hasClass($player, 'art-lock')
}
function setLock() {
addClass($player, 'art-lock')
art.isLock = true
art.emit('lock', true)
}
function setUnlock() {
removeClass($player, 'art-lock')
art.isLock = false
art.emit('lock', false)
}
layers.add({
name: 'lock',
mounted($el) {
const $lock = append($el, icons.lock)
const $unlock = append($el, icons.unlock)
setStyle($lock, 'display', 'none')
art.on('lock', (state) => {
if (state) {
setStyle($lock, 'display', 'inline-flex')
setStyle($unlock, 'display', 'none')
}
else {
setStyle($lock, 'display', 'none')
setStyle($unlock, 'display', 'inline-flex')
}
})
},
click() {
if (getState()) {
setUnlock()
}
else {
setLock()
}
},
})
return {
name: 'lock',
get state() {
return getState()
},
set state(value) {
if (value) {
setLock()
}
else {
setUnlock()
}
},
}
}
+94
View File
@@ -0,0 +1,94 @@
import type { SubscriptionHost } from '../component/resources'
import { appendElement } from '../component/dom'
import { entryScope, subscribeEntry } from '../component/resources'
import { isClosing } from '../lifecycle/instance'
import { addClass, hasClass, removeClass, setStyle } from '../utils'
export interface LockHost extends SubscriptionHost<{ lock: [boolean] }> {
template: { $player: HTMLElement }
icons: { lock: string | Element, unlock: string | Element }
isLock: boolean
emit: (name: 'lock', state: boolean) => unknown
layers: { add: (option: { name: string, mounted: (element: HTMLDivElement) => void, click: () => void }) => unknown }
}
export default function lock(art: LockHost): { name: string, state: boolean } {
const {
layers,
icons,
template: { $player },
} = art
function getState() {
return hasClass($player, 'art-lock')
}
function setLock() {
if (isClosing(art))
return
addClass($player, 'art-lock')
if (isClosing(art))
return
art.isLock = true
if (!isClosing(art))
art.emit('lock', true)
}
function setUnlock() {
if (isClosing(art))
return
removeClass($player, 'art-lock')
if (isClosing(art))
return
art.isLock = false
if (!isClosing(art))
art.emit('lock', false)
}
layers.add({
name: 'lock',
mounted($el) {
const scope = entryScope($el)
if (scope.closed || isClosing(art))
return
const $lock = appendElement($el, icons.lock)
if (scope.closed || isClosing(art))
return
const $unlock = appendElement($el, icons.unlock)
if (scope.closed || isClosing(art))
return
setStyle($lock, 'display', 'none')
subscribeEntry<{ lock: [boolean] }, 'lock'>(art, $el, 'lock', (state) => {
if (isClosing(art))
return
setStyle($lock, 'display', state ? 'inline-flex' : 'none')
if (!scope.closed && !isClosing(art))
setStyle($unlock, 'display', state ? 'none' : 'inline-flex')
})
},
click() {
if (getState()) {
setUnlock()
}
else {
setLock()
}
},
})
return {
name: 'lock',
get state() {
return getState()
},
set state(value: boolean) {
if (value) {
setLock()
}
else {
setUnlock()
}
},
}
}
@@ -1,14 +0,0 @@
import { addClass, removeClass } from '../utils'
export default function miniProgressBar(art) {
art.on('control', (state) => {
if (state) {
removeClass(art.template.$player, 'art-mini-progress-bar')
}
else {
addClass(art.template.$player, 'art-mini-progress-bar')
}
})
return { name: 'mini-progress-bar' }
}
@@ -0,0 +1,20 @@
import type { SubscriptionHost } from '../component/resources'
import { eventSubscriptions } from '../events/subscriptions'
import { addClass, removeClass } from '../utils'
export interface MiniProgressHost extends SubscriptionHost<{ control: [boolean] }> {
template: { $player: HTMLElement }
}
export default function miniProgressBar(art: MiniProgressHost): { name: string } {
eventSubscriptions<{ control: [boolean] }>(art)('control', (state) => {
if (state) {
removeClass(art.template.$player, 'art-mini-progress-bar')
}
else {
addClass(art.template.$player, 'art-mini-progress-bar')
}
})
return { name: 'mini-progress-bar' }
}