mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-20 16:49:52 +00:00
Improve how files get loaded
This commit is contained in:
parent
62a6dd17a2
commit
2bc90d9a90
12 changed files with 161 additions and 65 deletions
|
|
@ -58,7 +58,10 @@ import Winamp from 'winamp2-js';
|
|||
|
||||
const winamp = new Winamp({
|
||||
initialTrack: {
|
||||
name: "1. DJ Mike Llama - Llama Whippin' Intro",
|
||||
metaData: {
|
||||
artist: "DJ Mike Llama",
|
||||
title: "Llama Whippin' Intro",
|
||||
},
|
||||
url: "https://d38dnrh1liu4f5.cloudfront.net/projects/winamp2-js/mp3/llama-2.91.mp3"
|
||||
},
|
||||
initialSkin: {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { parser, creator } from "winamp-eqf";
|
|||
import {
|
||||
genArrayBufferFromFileReference,
|
||||
genArrayBufferFromUrl,
|
||||
promptForFileReferences
|
||||
promptForFileReferences,
|
||||
filenameFromUrl
|
||||
} from "./fileUtils";
|
||||
import skinParser from "./skinParser";
|
||||
import { BANDS, TRACK_HEIGHT } from "./constants";
|
||||
import { BANDS, TRACK_HEIGHT, LOAD_STYLE } from "./constants";
|
||||
import {
|
||||
getEqfData,
|
||||
nextTrack,
|
||||
|
|
@ -77,7 +78,7 @@ export function play() {
|
|||
state.playlist.curentTrack == null &&
|
||||
state.playlist.trackOrder.length === 0
|
||||
) {
|
||||
dispatch(openFileDialog());
|
||||
dispatch(openMediaFileDialog());
|
||||
} else {
|
||||
dispatch({ type: PLAY });
|
||||
}
|
||||
|
|
@ -196,20 +197,11 @@ function setEqFromFileReference(fileReference) {
|
|||
}
|
||||
|
||||
export function addTracksFromReferences(fileReferences, autoPlay, atIndex) {
|
||||
return dispatch => {
|
||||
if (autoPlay) {
|
||||
// I'm the worst. It just so happens that in every case that we autoPlay,
|
||||
// we should also clear all tracks.
|
||||
dispatch(removeAllTracks());
|
||||
}
|
||||
Array.from(fileReferences).forEach((file, i) => {
|
||||
const priority = i === 0 && autoPlay ? "PLAY" : "NONE";
|
||||
const id = uniqueId();
|
||||
const url = URL.createObjectURL(file);
|
||||
dispatch(_addTrackFromUrl(url, file.name, id, priority, atIndex + i));
|
||||
dispatch(fetchMediaTags(file, id));
|
||||
});
|
||||
};
|
||||
const tracks = Array.from(fileReferences).map(file => ({
|
||||
blob: file,
|
||||
defaultName: file.name
|
||||
}));
|
||||
return loadMediaFiles(tracks, autoPlay, atIndex);
|
||||
}
|
||||
|
||||
const SKIN_FILENAME_MATCHER = new RegExp("(wsz|zip)$", "i");
|
||||
|
|
@ -245,8 +237,18 @@ export function fetchMediaDuration(url, id) {
|
|||
const { duration } = audio;
|
||||
dispatch({ type: SET_MEDIA_DURATION, duration, id });
|
||||
audio.removeEventListener("durationchange", durationChange);
|
||||
audio.url = null;
|
||||
// TODO: Not sure if this really gets cleaned up.
|
||||
};
|
||||
audio.addEventListener("durationchange", durationChange);
|
||||
audio.addEventListener("error", () => {
|
||||
// TODO: Should we update the state to indicate that we don't know the length?
|
||||
/* Disabled, because people might drag in bogus local files.
|
||||
Raven.captureMessage(
|
||||
`Error getting duration of ${url}: ${audio.error.message}`
|
||||
);
|
||||
*/
|
||||
});
|
||||
audio.src = url;
|
||||
};
|
||||
}
|
||||
|
|
@ -256,29 +258,74 @@ function uniqueId() {
|
|||
return counter++;
|
||||
}
|
||||
|
||||
function _addTrackFromUrl(url, name, id, priority, atIndex) {
|
||||
/*
|
||||
type Track = {
|
||||
defaultName: ?string,
|
||||
duration: ?number,
|
||||
url?: string,
|
||||
blob?: fileReference
|
||||
}
|
||||
*/
|
||||
|
||||
export function loadMediaFiles(tracks, autoPlay = true, atIndex = 0) {
|
||||
return dispatch => {
|
||||
dispatch({ type: ADD_TRACK_FROM_URL, url, name, id, atIndex });
|
||||
switch (priority) {
|
||||
case "BUFFER":
|
||||
dispatch({ type: BUFFER_TRACK, name, id });
|
||||
break;
|
||||
case "PLAY":
|
||||
dispatch({ type: PLAY_TRACK, name, id });
|
||||
break;
|
||||
default:
|
||||
// If we're not going to load this right away,
|
||||
// we should fetch duration on our own
|
||||
dispatch(fetchMediaDuration(url, id));
|
||||
if (autoPlay) {
|
||||
// I'm the worst. It just so happens that in every case that we autoPlay,
|
||||
// we should also clear all tracks.
|
||||
dispatch(removeAllTracks());
|
||||
}
|
||||
tracks.forEach((track, i) => {
|
||||
const priority = i === 0 && autoPlay ? LOAD_STYLE.PLAY : null;
|
||||
dispatch(loadMediaFile(track, priority, atIndex));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMediaFromUrl(url, name, priority) {
|
||||
export function loadMediaFile(track, priority = null, atIndex = 0) {
|
||||
return dispatch => {
|
||||
const id = uniqueId();
|
||||
dispatch(_addTrackFromUrl(url, name, id, priority));
|
||||
dispatch(fetchMediaTags(url, id));
|
||||
const { url, blob, defaultName, metaData, duration } = track;
|
||||
let canonicalUrl = url;
|
||||
let canonicalDefaultName = defaultName;
|
||||
if (canonicalUrl == null) {
|
||||
if (blob == null) {
|
||||
throw new Error("Expected track to have either a blob or a url");
|
||||
}
|
||||
canonicalUrl = URL.createObjectURL(track.blob);
|
||||
} else if (!defaultName) {
|
||||
canonicalDefaultName = filenameFromUrl(url);
|
||||
// TODO: Derive the defaultName from the URL if none is provided
|
||||
}
|
||||
dispatch({
|
||||
type: ADD_TRACK_FROM_URL,
|
||||
url: canonicalUrl,
|
||||
name: canonicalDefaultName,
|
||||
id,
|
||||
atIndex
|
||||
});
|
||||
switch (priority) {
|
||||
case LOAD_STYLE.BUFFER:
|
||||
dispatch({ type: BUFFER_TRACK, id });
|
||||
break;
|
||||
case LOAD_STYLE.PLAY:
|
||||
dispatch({ type: PLAY_TRACK, id });
|
||||
break;
|
||||
default:
|
||||
// If we're not going to load this right away,
|
||||
// we should set duration on our own
|
||||
if (duration != null) {
|
||||
dispatch({ type: SET_MEDIA_DURATION, duration, id });
|
||||
} else {
|
||||
dispatch(fetchMediaDuration(canonicalUrl, id));
|
||||
}
|
||||
}
|
||||
|
||||
if (metaData != null) {
|
||||
const { artist, title } = metaData;
|
||||
dispatch({ type: SET_MEDIA_TAGS, artist, title, id });
|
||||
} else {
|
||||
dispatch(fetchMediaTags(url || blob, id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -339,13 +386,36 @@ export function setSkinFromUrl(url) {
|
|||
};
|
||||
}
|
||||
|
||||
export function openFileDialog(accept) {
|
||||
// This function is private, since Winamp consumers may wish to support
|
||||
// opening files via other means (Dropbox?). Only use the file type specific
|
||||
// versions below, since they can defer to the user-defined behavior.
|
||||
function _openFileDialog(accept) {
|
||||
return async dispatch => {
|
||||
const fileReferences = await promptForFileReferences(accept);
|
||||
dispatch(loadFilesFromReferences(fileReferences));
|
||||
};
|
||||
}
|
||||
|
||||
export function openEqfFileDialog() {
|
||||
return _openFileDialog(".eqf");
|
||||
}
|
||||
|
||||
export function openMediaFileDialog() {
|
||||
return async (dispatch, getState, { promptForMediaFiles }) => {
|
||||
if (promptForMediaFiles == null) {
|
||||
return dispatch(_openFileDialog());
|
||||
}
|
||||
const mediaFiles = await promptForMediaFiles();
|
||||
if (mediaFiles != null) {
|
||||
return dispatch(loadMediaFiles(mediaFiles, true, null));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function openSkinFileDialog() {
|
||||
return _openFileDialog(".zip, .wsz");
|
||||
}
|
||||
|
||||
export function setEqBand(band, value) {
|
||||
return { type: SET_BAND_VALUE, band, value };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { openFileDialog, downloadPreset } from "../../actionCreators";
|
||||
import { openEqfFileDialog, downloadPreset } from "../../actionCreators";
|
||||
import { ContextMenu, Node } from "../ContextMenu";
|
||||
|
||||
const MainContextMenu = props => (
|
||||
<ContextMenu top id="presets-context" handle={<div id="presets" />}>
|
||||
<Node onClick={props.openFileDialog} label="Load" />
|
||||
<Node onClick={props.openEqfFileDialog} label="Load" />
|
||||
<Node onClick={props.downloadPreset} label="Save" />
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openFileDialog: () => openFileDialog(".eqf"),
|
||||
downloadPreset
|
||||
};
|
||||
const mapDispatchToProps = { openEqfFileDialog, downloadPreset };
|
||||
|
||||
export default connect(null, mapDispatchToProps)(MainContextMenu);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { openFileDialog } from "../../actionCreators";
|
||||
import { openMediaFileDialog } from "../../actionCreators";
|
||||
|
||||
const Eject = props => (
|
||||
<div id="eject" onClick={props.openFileDialog} title="Open File(s)" />
|
||||
<div id="eject" onClick={props.openMediaFileDialog} title="Open File(s)" />
|
||||
);
|
||||
|
||||
const mapDispatchToProps = { openFileDialog };
|
||||
const mapDispatchToProps = { openMediaFileDialog };
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Eject);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import ClickedDiv from "../ClickedDiv";
|
||||
import { close, setSkinFromUrl, openFileDialog } from "../../actionCreators";
|
||||
import {
|
||||
close,
|
||||
setSkinFromUrl,
|
||||
openMediaFileDialog,
|
||||
openSkinFileDialog
|
||||
} from "../../actionCreators";
|
||||
import { ContextMenu, Hr, Node, Parent, LinkNode } from "../ContextMenu";
|
||||
|
||||
const MainContextMenu = props => (
|
||||
|
|
@ -16,9 +21,9 @@ const MainContextMenu = props => (
|
|||
label="Winamp2-js"
|
||||
/>
|
||||
<Hr />
|
||||
<Node onClick={props.openFileDialogForMedia} label="Play File..." />
|
||||
<Node onClick={props.openMediaFileDialog} label="Play File..." />
|
||||
<Parent label="Skins">
|
||||
<Node onClick={props.openFileDialogForSkin} label="Load Skin..." />
|
||||
<Node onClick={props.openSkinFileDialog} label="Load Skin..." />
|
||||
{!!props.avaliableSkins.length && <Hr />}
|
||||
{props.avaliableSkins.map(skin => (
|
||||
<Node
|
||||
|
|
@ -39,8 +44,8 @@ const mapStateToProps = state => ({
|
|||
|
||||
const mapDispatchToProps = {
|
||||
close,
|
||||
openFileDialogForSkin: () => openFileDialog(".zip, .wsz"),
|
||||
openFileDialogForMedia: openFileDialog,
|
||||
openSkinFileDialog,
|
||||
openMediaFileDialog,
|
||||
setSkin: setSkinFromUrl
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
stop,
|
||||
next,
|
||||
previous,
|
||||
openFileDialog
|
||||
openMediaFileDialog
|
||||
} from "../../actionCreators";
|
||||
|
||||
import MiniTime from "../MiniTime";
|
||||
|
|
@ -22,7 +22,10 @@ const PlaylistWindow = props => (
|
|||
<div className="playlist-pause-button" onClick={props.pause} />
|
||||
<div className="playlist-stop-button" onClick={props.stop} />
|
||||
<div className="playlist-next-button" onClick={props.next} />
|
||||
<div className="playlist-eject-button" onClick={props.openFileDialog} />
|
||||
<div
|
||||
className="playlist-eject-button"
|
||||
onClick={props.openMediaFileDialog}
|
||||
/>
|
||||
</div>
|
||||
<MiniTime />
|
||||
</React.Fragment>
|
||||
|
|
@ -32,7 +35,7 @@ const mapDispatchToProps = {
|
|||
play,
|
||||
pause,
|
||||
stop,
|
||||
openFileDialog,
|
||||
openMediaFileDialog,
|
||||
next,
|
||||
previous
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import { Provider } from "react-redux";
|
||||
import renderer from "react-test-renderer";
|
||||
import getStore from "../../store";
|
||||
import { loadMediaFromUrl } from "../../actionCreators";
|
||||
import { loadMediaFiles } from "../../actionCreators";
|
||||
|
||||
import PlaylistShade from "./PlaylistShade";
|
||||
|
||||
|
|
@ -21,7 +21,9 @@ describe("PlaylistShade", () => {
|
|||
});
|
||||
|
||||
it("renders to snapshot", () => {
|
||||
store.dispatch(loadMediaFromUrl("http://example.com", "Some Name", "NONE"));
|
||||
store.dispatch(
|
||||
loadMediaFiles([{ url: "http://example.com", defaultName: "Some Name" }])
|
||||
);
|
||||
const tree = renderer
|
||||
.create(
|
||||
<Provider store={store}>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ export const WINDOWS = {
|
|||
EQUALIZER: "EQUALIZER"
|
||||
};
|
||||
|
||||
export const LOAD_STYLE = {
|
||||
BUFFER: "BUFFER",
|
||||
PLAY: "PLAY"
|
||||
};
|
||||
|
||||
export const UTF8_ELLIPSIS = "\u2026";
|
||||
export const CHARACTER_WIDTH = 5;
|
||||
export const PLAYLIST_RESIZE_SEGMENT_WIDTH = 25;
|
||||
|
|
|
|||
|
|
@ -47,3 +47,12 @@ export async function promptForFileReferences(accept) {
|
|||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
// This is not perfect, but... meh: https://stackoverflow.com/a/36756650/1263117
|
||||
export function filenameFromUrl(url) {
|
||||
return url
|
||||
.split("/")
|
||||
.pop()
|
||||
.split("#")[0]
|
||||
.split("?")[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
adjustVolume,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
openFileDialog,
|
||||
openMediaFileDialog,
|
||||
seekForward,
|
||||
seekBackward,
|
||||
reverseList,
|
||||
|
|
@ -72,7 +72,7 @@ export default function(dispatch) {
|
|||
dispatch(pause());
|
||||
break;
|
||||
case 76: // L
|
||||
dispatch(openFileDialog());
|
||||
dispatch(openMediaFileDialog());
|
||||
break;
|
||||
case 82: // R
|
||||
dispatch(toggleRepeat());
|
||||
|
|
@ -90,7 +90,7 @@ export default function(dispatch) {
|
|||
dispatch(previous());
|
||||
break;
|
||||
case 96: // numpad 0
|
||||
dispatch(openFileDialog());
|
||||
dispatch(openMediaFileDialog());
|
||||
break;
|
||||
case 97: // numpad 1
|
||||
dispatch(nextN(-10));
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ Raven.context(() => {
|
|||
url: skinUrl
|
||||
},
|
||||
initialTrack: {
|
||||
name: "DJ Mike Llama - Llama Whippin' Intro",
|
||||
defaultName: "DJ Mike Llama - Llama Whippin' Intro",
|
||||
metaData: {
|
||||
artist: "DJ Mike Llama",
|
||||
title: "Llama Whippin' Intro"
|
||||
},
|
||||
url: audioUrl
|
||||
},
|
||||
avaliableSkins: [
|
||||
|
|
|
|||
12
js/winamp.js
12
js/winamp.js
|
|
@ -6,7 +6,8 @@ import getStore from "./store";
|
|||
import App from "./components/App";
|
||||
import Hotkeys from "./hotkeys";
|
||||
import Media from "./media";
|
||||
import { setSkinFromUrl, loadMediaFromUrl } from "./actionCreators";
|
||||
import { setSkinFromUrl, loadMediaFile } from "./actionCreators";
|
||||
import { LOAD_STYLE } from "./constants";
|
||||
|
||||
import { SET_AVALIABLE_SKINS } from "./actionTypes";
|
||||
|
||||
|
|
@ -39,17 +40,14 @@ class Winamp {
|
|||
this.options = options;
|
||||
|
||||
this.media = new Media();
|
||||
this.store = getStore(this.media, this.options.__initialState);
|
||||
this.store = getStore(this.media, this.options);
|
||||
|
||||
this.store.dispatch(setSkinFromUrl(this.options.initialSkin.url));
|
||||
|
||||
// TODO: Make this initial track_s_
|
||||
if (this.options.initialTrack && this.options.initialTrack.url) {
|
||||
this.store.dispatch(
|
||||
loadMediaFromUrl(
|
||||
this.options.initialTrack.url,
|
||||
this.options.initialTrack.name,
|
||||
"BUFFER"
|
||||
)
|
||||
loadMediaFile(this.options.initialTrack, LOAD_STYLE.BUFFER)
|
||||
);
|
||||
}
|
||||
if (this.options.avaliableSkins) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue