import * as React from "react" ; import ReactDOM from "react-dom" ; import { Provider } from "react-redux" ; import { Store , AppState , Track , LoadedURLTrack , Middleware , WindowPosition , ButterchurnOptions , PartialState , } from "./types" ; import getStore from "./store" ; import App from "./components/App" ; import { bindHotkeys } from "./hotkeys" ; import Media from "./media" ; import * as Selectors from "./selectors" ; import * as Actions from "./actionCreators" ; import { LOAD_STYLE } from "./constants" ; import * as Utils from "./utils" ; import * as FileUtils from "./fileUtils" ; import { SET_AVAILABLE_SKINS , NETWORK_CONNECTED , NETWORK_DISCONNECTED , CLOSE_WINAMP , MINIMIZE_WINAMP , LOADED , SET_Z_INDEX , CLOSE_REQUESTED , ENABLE_MILKDROP , } from "./actionTypes" ; import Emitter from "./emitter" ; import { SerializedStateV1 } from "./serializedStates/v1Types" ; import Disposable from "./Disposable" ; export interface Options { /**
     * An object representing the initial skin to use.
     *
     * If omitted, the default skin, included in the bundle, will be used.
     * Note: This URL must be served the with correct CORs headers.
     *
     * Example: `{ url: './path/to/skin.wsz' }`
     */ initialSkin ? : { url : string ; } ; /**
     * An array of `Track`s to prepopulate the playlist with.
     */ initialTracks ? : Track [ ] ; /**
     * An array of objects representing available skins.
     *
     * These will appear in the "Options" menu under "Skins".
     * Note: These URLs must be served with the correct CORs headers.
     *
     * Example: `[ { url: "./green.wsz", name: "Green Dimension V2" } ]`
     */ availableSkins ? : { url : string ; name : string ; } [ ] ; /**
     * Should global hotkeys be enabled?
     *
     * Default: `false`
     */ enableHotkeys ? : boolean ; /**
     * An array of additional file pickers.
     *
     * These will appear in the "Options" menu under "Play".
     *
     * In the offical version, this option is used to provide a "Dropbox" file picker.
     */ filePickers ? : [ { /**
             * The name that will appear in the context menu.
             *
             * Example: `"My File Picker..."`
             */ contextMenuName : string ; /**
             * A function which returns a Promise that resolves to an array of `Track`s
             *
             * Example: `() => Promise.resolve([{ url: './rick_roll.mp3' }])`
             */ filePicker : ( ) => Promise < Track [ ] > ; /**
             * Indicates if this options should be made available when the user is offline.
             */ requiresNetwork : boolean ; } ] ; zIndex ? : number ; handleTrackDropEvent ? : ( e : React . DragEvent < HTMLDivElement > ) => Track [ ] | null | Promise < Track [ ] | null > ; handleAddUrlEvent ? : ( ) => Track [ ] | null | Promise < Track [ ] | null > ; handleLoadListEvent ? : ( ) => Track [ ] | null | Promise < Track [ ] | null > ; handleSaveListEvent ? : ( tracks : Track [ ] ) => null | Promise < null > ; } export type WindowLayout = { [ windowId ] : { size ? : null | [ number , number ] ; position : WindowPosition ; } ; } ; export interface PrivateOptions { avaliableSkins ? : { url : string ; name : string ; } [ ] ; requireJSZip ( ) : Promise < any > ; requireMusicMetadata ( ) : Promise < any > ; __initialState ? : PartialState ; __customMiddlewares ? : Middleware [ ] ; __initialWindowLayout ? : WindowLayout ; __butterchurnOptions ? : ButterchurnOptions ; __customMediaClass ? : typeof Media ; } const storeHas = ( store , predicate ) : Promise < void > => new Promise ( ( resolve ) => { if ( predicate ( store . getState ( ) ) ) { resolve ( ) ; return ; } const unsubscribe = store . subscribe ( ( ) => { if ( predicate ( store . getState ( ) ) ) { resolve ( ) ; unsubscribe ( ) ; } } ) ; } ) ; class Webamp { static VERSION = "1.5.0" ; _actionEmitter : Emitter ; _node : HTMLElement | null ; _disposable : Disposable ; options : Options & PrivateOptions ; media : Media ; store : Store ; static browserIsSupported ( ) { const supportsAudioApi = ! ! ( window . AudioContext || window . webkitAudioContext ) ; const supportsCanvas = ! ! window . document . createElement ( "canvas" ) . getContext ; const supportsPromises = typeof Promise !== "undefined" ; return supportsAudioApi && supportsCanvas && supportsPromises ; } constructor ( options ) { this . _node = null ; this . _disposable = new Disposable ( ) ; this . _actionEmitter = new Emitter ( ) ; this . options = options ; const { initialTracks , initialSkin , avaliableSkins , availableSkins , enableHotkeys = false , zIndex , requireJSZip , requireMusicMetadata , handleTrackDropEvent , handleAddUrlEvent , handleLoadListEvent , handleSaveListEvent , __butterchurnOptions , __customMediaClass , } = this . options ; let convertPreset = null ; if ( __butterchurnOptions != null ) { const { importConvertPreset , presetConverterEndpoint } = __butterchurnOptions ; if ( importConvertPreset != null && presetConverterEndpoint != null ) { convertPreset = async ( file ) : Promise < Object > => { const { convertPreset : convert } = await importConvertPreset ( ) ; return convert ( await FileUtils . genStringFromFileReference ( file ) , presetConverterEndpoint ) ; } ; } } this . media = new ( __customMediaClass || Media ) ( ) ; this . store = getStore ( this . media , this . _actionEmitter , this . options . __customMiddlewares , this . options . __initialState , { requireJSZip , requireMusicMetadata , convertPreset , handleTrackDropEvent , handleAddUrlEvent , handleLoadListEvent , handleSaveListEvent , } ) as Store ; if ( navigator . onLine ) { this . store . dispatch ( { type : NETWORK_CONNECTED } ) ; } else { this . store . dispatch ( { type : NETWORK_DISCONNECTED } ) ; } if ( zIndex != null ) { this . store . dispatch ( { type : SET_Z_INDEX , zIndex } ) ; } if ( options . __butterchurnOptions ) { this . store . dispatch ( { type : ENABLE_MILKDROP , open : options . __butterchurnOptions . butterchurnOpen , } ) ; this . store . dispatch ( Actions . initializePresets ( options . __butterchurnOptions ) ) ; } const handleOnline = ( ) => this . store . dispatch ( { type : NETWORK_CONNECTED } ) ; const handleOffline = ( ) => this . store . dispatch ( { type : NETWORK_DISCONNECTED } ) ; window . addEventListener ( "online" , handleOnline ) ; window . addEventListener ( "offline" , handleOffline ) ; this . _disposable . add ( ( ) => { window . removeEventListener ( "online" , handleOnline ) ; window . removeEventListener ( "offline" , handleOffline ) ; } ) ; if ( initialSkin ) { this . store . dispatch ( Actions . setSkinFromUrl ( initialSkin . url ) ) ; } else { this . store . dispatch ( { type : LOADED } ) ; } if ( initialTracks ) { this . _bufferTracks ( initialTracks ) ; } if ( avaliableSkins != null ) { console . warn ( "The misspelled option `avaliableSkins` is deprecated. Please use `availableSkins` instead." ) ; this . store . dispatch ( { type : SET_AVAILABLE_SKINS , skins : avaliableSkins } ) ; } else if ( availableSkins != null ) { this . store . dispatch ( { type : SET_AVAILABLE_SKINS , skins : availableSkins } ) ; } const layout = options . __initialWindowLayout ; if ( layout == null ) { this . store . dispatch ( Actions . stackWindows ( ) ) ; } else { Utils . objectForEach ( layout , ( w , windowId ) => { if ( w . size != null ) { this . store . dispatch ( Actions . setWindowSize ( windowId , w . size ) ) ; } } ) ; this . store . dispatch ( Actions . updateWindowPositions ( Utils . objectMap ( layout , ( w ) => w . position ) , false ) ) ; } if ( enableHotkeys ) { this . _disposable . add ( bindHotkeys ( this . store . dispatch ) ) ; } } play ( ) { this . store . dispatch ( Actions . play ( ) ) ; } pause ( ) { this . store . dispatch ( Actions . pause ( ) ) ; } stop ( ) { this . store . dispatch ( Actions . stop ( ) ) ; } seekBackward ( seconds ) { this . store . dispatch ( Actions . seekBackward ( seconds ) ) ; } seekForward ( seconds ) { this . store . dispatch ( Actions . seekForward ( seconds ) ) ; } seekToTime ( seconds ) { this . store . dispatch ( Actions . seekToTime ( seconds ) ) ; } nextTrack ( ) { this . store . dispatch ( Actions . next ( ) ) ; } previousTrack ( ) { this . store . dispatch ( Actions . previous ( ) ) ; } _bufferTracks ( tracks ) { const nextIndex = Selectors . getTrackCount ( this . store . getState ( ) ) ; this . store . dispatch ( Actions . loadMediaFiles ( tracks , LOAD_STYLE . BUFFER , nextIndex ) ) ; } appendTracks ( tracks ) { const nextIndex = Selectors . getTrackCount ( this . store . getState ( ) ) ; this . store . dispatch ( Actions . loadMediaFiles ( tracks , LOAD_STYLE . NONE , nextIndex ) ) ; } setTracksToPlay ( tracks ) { this . store . dispatch ( Actions . loadMediaFiles ( tracks , LOAD_STYLE . PLAY ) ) ; } getMediaStatus ( ) { return Selectors . getMediaStatus ( this . store . getState ( ) ) ; } onWillClose ( cb ) { return this . _actionEmitter . on ( CLOSE_REQUESTED , ( action ) => { cb ( action . cancel ) ; } ) ; } onClose ( cb ) { return this . _actionEmitter . on ( CLOSE_WINAMP , cb ) ; } close ( ) { this . store . dispatch ( Actions . close ( ) ) ; } reopen ( ) { this . store . dispatch ( Actions . open ( ) ) ; } onTrackDidChange ( cb ) { let previousTrackId = null ; return this . store . subscribe ( ( ) => { const state = this . store . getState ( ) ; const trackId = Selectors . getCurrentlyPlayingTrackIdIfLoaded ( state ) ; if ( trackId === previousTrackId ) { return ; } previousTrackId = trackId ; cb ( trackId == null ? null : Selectors . getCurrentTrackInfo ( state ) ) ; } ) ; } onMinimize ( cb ) { return this . _actionEmitter . on ( MINIMIZE_WINAMP , cb ) ; } setSkinFromUrl ( url ) { this . store . dispatch ( Actions . setSkinFromUrl ( url ) ) ; } async skinIsLoaded ( ) { await storeHas ( this . store , ( state ) => ! state . display . loading ) ; await Promise . all ( [ Selectors . getPreampLineImage ( this . store . getState ( ) ) , Selectors . getLineColorsImage ( this . store . getState ( ) ) , ] ) ; return ; } __loadSerializedState ( serializedState ) { this . store . dispatch ( Actions . loadSerializedState ( serializedState ) ) ; } __getSerializedState ( ) { return Selectors . getSerlializedState ( this . store . getState ( ) ) ; } __onStateChange ( cb ) { return this . store . subscribe ( cb ) ; } async renderWhenReady ( node ) { this . store . dispatch ( Actions . centerWindowsInContainer ( node ) ) ; await this . skinIsLoaded ( ) ; if ( this . _node != null ) { throw new Error ( "Cannot render a Webamp instance twice" ) ; } this . _node = node ; this . _disposable . add ( ( ) => { if ( this . _node != null ) { ReactDOM . unmountComponentAtNode ( this . _node ) ; this . _node = null ; } } ) ; const root = ReactDOM . createRoot ( node ) ; root . render ( < Provider store = { this . store } > < App media = { this . media } filePickers = { this . options . filePickers || [ ] } / > < /Provider>); } dispose ( ) { this . media . dispose ( ) ; this . _actionEmitter . dispose ( ) ; this . _disposable . dispose ( ) ; } } export default Webamp ;