mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 10:31:20 -08:00
refactor(core): CORE-18 type builtins and own prompt lifetimes
This commit is contained in:
1 parent
a842058136
commit
98a1800032
55 files changed
+26511
-650
No files matched your search
@@ -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
|
||||
|
||||
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 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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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' }
|
||||
}
|
||||
Reference in new issue
Block a user