mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-27 03:54:24 +00:00
Merges upstream/master and resolves conflicts
This commit is contained in:
commit
1c97cce4e1
28 changed files with 415 additions and 100 deletions
|
|
@ -2,3 +2,4 @@
|
|||
built/
|
||||
coverage/
|
||||
**/node_modules/
|
||||
experiments/winamp-skin-museum/
|
||||
|
|
@ -33,12 +33,7 @@
|
|||
"camelcase": "error",
|
||||
"consistent-return": "warn",
|
||||
"constructor-super": "error",
|
||||
"dot-notation": [
|
||||
"error",
|
||||
{
|
||||
"allowKeywords": false
|
||||
}
|
||||
],
|
||||
"dot-notation": "error",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"guard-for-in": "error",
|
||||
"max-depth": ["warn", 4],
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@ node_modules
|
|||
|
||||
/built
|
||||
/coverage
|
||||
/experiments/winamp-skin-museum
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ const config = {
|
|||
config.skinUrl = skinUrl;
|
||||
const url = `http://localhost:8080/#${JSON.stringify(config)}`;
|
||||
console.log({ url });
|
||||
await page["goto"](url);
|
||||
await page.goto(url);
|
||||
await page.waitForSelector("#main-window", { timeout: 2000 });
|
||||
|
||||
console.log("Writing screenshot to", screenshotFile);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
<a href='mailto:jordan@jordaneldredge.com?subject=Winamp2-js%20Feedback'>Feedback</a> |
|
||||
<a href='https://github.com/captbaritone/winamp2-js'>GitHub</a>
|
||||
</p>
|
||||
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="7py29249dpeddu8"></script>
|
||||
<script src="built/winamp.js"></script>
|
||||
</body>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import jsmediatags from "jsmediatags/dist/jsmediatags";
|
||||
import { parser, creator } from "winamp-eqf";
|
||||
import {
|
||||
genArrayBufferFromFileReference,
|
||||
genArrayBufferFromUrl,
|
||||
promptForFileReferences
|
||||
promptForFileReferences,
|
||||
genMediaDuration,
|
||||
genMediaTags
|
||||
} from "./fileUtils";
|
||||
import skinParser from "./skinParser";
|
||||
import { BANDS, TRACK_HEIGHT, LOAD_STYLE } from "./constants";
|
||||
|
|
@ -13,7 +14,9 @@ import {
|
|||
getScrollOffset,
|
||||
getOverflowTrackCount,
|
||||
getPlaylistURL,
|
||||
getSelectedTrackObjects
|
||||
getSelectedTrackObjects,
|
||||
getTracks,
|
||||
getTrackIsVisibleFunction
|
||||
} from "./selectors";
|
||||
|
||||
import {
|
||||
|
|
@ -53,9 +56,22 @@ import {
|
|||
SET_MEDIA_TAGS,
|
||||
SET_MEDIA_DURATION,
|
||||
TOGGLE_SHADE_MODE,
|
||||
TOGGLE_PLAYLIST_SHADE_MODE
|
||||
TOGGLE_PLAYLIST_SHADE_MODE,
|
||||
MEDIA_TAG_REQUEST_INITIALIZED,
|
||||
MEDIA_TAG_REQUEST_FAILED,
|
||||
PLAYLIST_SIZE_CHANGED
|
||||
} from "./actionTypes";
|
||||
|
||||
import LoadQueue from "./loadQueue";
|
||||
|
||||
// Lower is better
|
||||
const DURATION_VISIBLE_PRIORITY = 5;
|
||||
const META_DATA_VISIBLE_PRIORITY = 10;
|
||||
const DURATION_PRIORITY = 15;
|
||||
const META_DATA_PRIORITY = 20;
|
||||
|
||||
const loadQueue = new LoadQueue({ threads: 4 });
|
||||
|
||||
function playRandomTrack() {
|
||||
return (dispatch, getState) => {
|
||||
const { playlist: { trackOrder, currentTrack } } = getState();
|
||||
|
|
@ -228,22 +244,23 @@ export function loadFilesFromReferences(
|
|||
}
|
||||
|
||||
export function fetchMediaDuration(url, id) {
|
||||
return dispatch => {
|
||||
// TODO: Does this actually stop downloading the file once it's
|
||||
// got the duration?
|
||||
const audio = document.createElement("audio");
|
||||
const durationChange = () => {
|
||||
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?
|
||||
});
|
||||
audio.src = url;
|
||||
return (dispatch, getState) => {
|
||||
loadQueue.push(
|
||||
() =>
|
||||
genMediaDuration(url)
|
||||
.then(duration =>
|
||||
dispatch({ type: SET_MEDIA_DURATION, duration, id })
|
||||
)
|
||||
.catch(() => {
|
||||
// TODO: Should we update the state to indicate that we don't know the length?
|
||||
}),
|
||||
() => {
|
||||
const trackIsVisible = getTrackIsVisibleFunction(getState());
|
||||
return trackIsVisible(id)
|
||||
? DURATION_VISIBLE_PRIORITY
|
||||
: DURATION_PRIORITY;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +278,7 @@ export function loadMediaFiles(tracks, loadStyle = null, atIndex = 0) {
|
|||
}
|
||||
tracks.forEach((track, i) => {
|
||||
const priority = i === 0 && loadStyle != null ? loadStyle : null;
|
||||
dispatch(loadMediaFile(track, priority, atIndex));
|
||||
dispatch(loadMediaFile(track, priority, atIndex + i));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -304,35 +321,43 @@ export function loadMediaFile(track, priority = null, atIndex = 0) {
|
|||
if (metaData != null) {
|
||||
const { artist, title } = metaData;
|
||||
dispatch({ type: SET_MEDIA_TAGS, artist, title, id });
|
||||
} else if (blob != null) {
|
||||
// Blobs can be loaded quickly
|
||||
dispatch(fetchMediaTags(blob, id));
|
||||
} else {
|
||||
dispatch(fetchMediaTags(url || blob, id));
|
||||
dispatch(queueFetchingMediaTags(id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function queueFetchingMediaTags(id) {
|
||||
return (dispatch, getState) => {
|
||||
const track = getTracks(getState())[id];
|
||||
return loadQueue.push(
|
||||
() => dispatch(fetchMediaTags(track.url, id)),
|
||||
() => {
|
||||
const trackIsVisible = getTrackIsVisibleFunction(getState());
|
||||
return trackIsVisible(track.id)
|
||||
? META_DATA_VISIBLE_PRIORITY
|
||||
: META_DATA_PRIORITY;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMediaTags(file, id) {
|
||||
// Workaround https://github.com/aadsm/jsmediatags/issues/83
|
||||
if (typeof file === "string" && !/^[a-z]+:\/\//i.test(file)) {
|
||||
file = `${location.protocol}//${location.host}${location.pathname}${file}`;
|
||||
}
|
||||
return dispatch => {
|
||||
try {
|
||||
jsmediatags.read(file, {
|
||||
onSuccess: data => {
|
||||
const { artist, title } = data.tags;
|
||||
// There's more data here, but we don't have a use for it yet:
|
||||
// https://github.com/aadsm/jsmediatags#shortcuts
|
||||
dispatch({ type: SET_MEDIA_TAGS, artist, title, id });
|
||||
},
|
||||
onError: () => {
|
||||
// Nothing to do. The filename will have to suffice.
|
||||
}
|
||||
dispatch({ type: MEDIA_TAG_REQUEST_INITIALIZED, id });
|
||||
return genMediaTags(file)
|
||||
.then(data => {
|
||||
const { artist, title } = data.tags;
|
||||
// There's more data here, but we don't have a use for it yet:
|
||||
// https://github.com/aadsm/jsmediatags#shortcuts
|
||||
dispatch({ type: SET_MEDIA_TAGS, artist, title, id });
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({ type: MEDIA_TAG_REQUEST_FAILED, id });
|
||||
});
|
||||
} catch (e) {
|
||||
// Possibly jsmediatags could not find a parser for this file?
|
||||
// Nothing to do.
|
||||
// Consider removing this after https://github.com/aadsm/jsmediatags/issues/83 is resolved.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -367,12 +392,12 @@ export function setSkinFromUrl(url) {
|
|||
};
|
||||
}
|
||||
|
||||
// This function is private, since Winamp consumers may wish to support
|
||||
// opening files via other means (Dropbox?). Only use the file type specific
|
||||
// This function is private, since Winamp consumers can provide means for
|
||||
// opening files via other methods. 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);
|
||||
const fileReferences = await promptForFileReferences({ accept });
|
||||
dispatch(loadFilesFromReferences(fileReferences));
|
||||
};
|
||||
}
|
||||
|
|
@ -514,6 +539,10 @@ export function setPlaylistScrollPosition(position) {
|
|||
return { type: SET_PLAYLIST_SCROLL_POSITION, position };
|
||||
}
|
||||
|
||||
export function setPlaylistSize(size) {
|
||||
return { type: PLAYLIST_SIZE_CHANGED, size };
|
||||
}
|
||||
|
||||
export function scrollNTracks(n) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
|
|
|||
|
|
@ -60,3 +60,5 @@ export const SET_MEDIA_TAGS = "SET_MEDIA_TAGS";
|
|||
export const SET_MEDIA_DURATION = "SET_MEDIA_DURATION";
|
||||
export const OPEN_GEN_WINDOW = "OPEN_GEN_WINDOW";
|
||||
export const CLOSE_GEN_WINDOW = "CLOSE_GEN_WINDOW";
|
||||
export const MEDIA_TAG_REQUEST_INITIALIZED = "MEDIA_TAG_REQUEST_INITIALIZED";
|
||||
export const MEDIA_TAG_REQUEST_FAILED = "MEDIA_TAG_REQUEST_FAILED";
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@ const App = ({
|
|||
equalizer,
|
||||
playlist,
|
||||
openWindows,
|
||||
container
|
||||
container,
|
||||
filePickers
|
||||
}) => {
|
||||
if (closed) {
|
||||
return null;
|
||||
}
|
||||
const windows = {
|
||||
main: <MainWindow mediaPlayer={media} />,
|
||||
main: <MainWindow mediaPlayer={media} filePickers={filePickers} />,
|
||||
equalizer: equalizer && <EqualizerWindow />,
|
||||
playlist: playlist && <PlaylistWindow />
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import {
|
|||
close,
|
||||
setSkinFromUrl,
|
||||
openMediaFileDialog,
|
||||
loadMediaFiles,
|
||||
openSkinFileDialog
|
||||
} from "../../actionCreators";
|
||||
import { LOAD_STYLE } from "../../constants";
|
||||
import { ContextMenu, Hr, Node, Parent, LinkNode } from "../ContextMenu";
|
||||
|
||||
const MainContextMenu = props => (
|
||||
|
|
@ -21,7 +23,19 @@ const MainContextMenu = props => (
|
|||
label="Winamp2-js"
|
||||
/>
|
||||
<Hr />
|
||||
<Node onClick={props.openMediaFileDialog} label="Play File..." />
|
||||
<Parent label="Play">
|
||||
<Node onClick={props.openMediaFileDialog} label="File..." />
|
||||
{props.filePickers &&
|
||||
props.filePickers.map((picker, i) => (
|
||||
<Node
|
||||
key={i}
|
||||
onClick={async () => {
|
||||
props.loadMediaFiles(await picker.filePicker(), LOAD_STYLE.PLAY);
|
||||
}}
|
||||
label={picker.contextMenuName}
|
||||
/>
|
||||
))}
|
||||
</Parent>
|
||||
<Parent label="Skins">
|
||||
<Node onClick={props.openSkinFileDialog} label="Load Skin..." />
|
||||
{!!props.avaliableSkins.length && <Hr />}
|
||||
|
|
@ -46,6 +60,7 @@ const mapDispatchToProps = {
|
|||
close,
|
||||
openSkinFileDialog,
|
||||
openMediaFileDialog,
|
||||
loadMediaFiles,
|
||||
setSkin: setSkinFromUrl
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ export class MainWindow extends React.Component {
|
|||
shade,
|
||||
llama,
|
||||
status,
|
||||
working
|
||||
working,
|
||||
filePickers
|
||||
} = this.props;
|
||||
|
||||
const className = classnames({
|
||||
|
|
@ -89,7 +90,7 @@ export class MainWindow extends React.Component {
|
|||
className="selected title-bard draggable"
|
||||
onDoubleClick={this.props.toggleMainWindowShadeMode}
|
||||
>
|
||||
<MainContextMenu />
|
||||
<MainContextMenu filePickers={filePickers} />
|
||||
{shade && <MiniTime />}
|
||||
<Minimize />
|
||||
<Shade />
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ import { addTracksFromReferences } from "../../actionCreators";
|
|||
import { promptForFileReferences } from "../../fileUtils";
|
||||
import PlaylistMenu from "./PlaylistMenu";
|
||||
|
||||
const el = document.createElement("input");
|
||||
el.type = "file";
|
||||
const DIR_SUPPORT =
|
||||
typeof el.webkitdirectory !== "undefined" ||
|
||||
typeof el.mozdirectory !== "undefined" ||
|
||||
typeof el.directory !== "undefined";
|
||||
|
||||
/* eslint-disable no-alert */
|
||||
|
||||
const AddMenu = ({ nextIndex, addFilesAtIndex }) => (
|
||||
const AddMenu = ({ nextIndex, addFilesAtIndex, addDirAtIndex }) => (
|
||||
<PlaylistMenu id="playlist-add-menu">
|
||||
<div
|
||||
className="add-url"
|
||||
onClick={() => alert("Not supported in Winamp2-js")}
|
||||
/>
|
||||
<div
|
||||
className="add-dir"
|
||||
onClick={() => alert("Not supported in Winamp2-js")}
|
||||
/>
|
||||
<div className="add-dir" onClick={() => addDirAtIndex(nextIndex)} />
|
||||
<div className="add-file" onClick={() => addFilesAtIndex(nextIndex)} />
|
||||
</PlaylistMenu>
|
||||
);
|
||||
|
|
@ -28,6 +32,14 @@ const mapDispatchToProps = dispatch => ({
|
|||
addFilesAtIndex: async nextIndex => {
|
||||
const fileReferences = await promptForFileReferences();
|
||||
dispatch(addTracksFromReferences(fileReferences, null, nextIndex));
|
||||
},
|
||||
addDirAtIndex: async nextIndex => {
|
||||
if (!DIR_SUPPORT) {
|
||||
alert("Not supported in your browser");
|
||||
return;
|
||||
}
|
||||
const fileReferences = await promptForFileReferences({ directory: true });
|
||||
dispatch(addTracksFromReferences(fileReferences, null, nextIndex));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from "react-redux";
|
||||
import ResizeTarget from "../ResizeTarget";
|
||||
import { PLAYLIST_SIZE_CHANGED } from "../../actionTypes";
|
||||
import { setPlaylistSize } from "../../actionCreators";
|
||||
import {
|
||||
PLAYLIST_RESIZE_SEGMENT_WIDTH,
|
||||
PLAYLIST_RESIZE_SEGMENT_HEIGHT
|
||||
|
|
@ -13,8 +13,6 @@ const mapStateToProps = state => ({
|
|||
id: "playlist-resize-target"
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setPlaylistSize: size => ({ type: PLAYLIST_SIZE_CHANGED, size })
|
||||
};
|
||||
const mapDispatchToProps = { setPlaylistSize };
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ResizeTarget);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
togglePlaylistShadeMode,
|
||||
scrollVolume
|
||||
} from "../../actionCreators";
|
||||
import { getScrollOffset } from "../../selectors";
|
||||
|
||||
import { clamp } from "../../utils";
|
||||
import DropTarget from "../DropTarget";
|
||||
|
|
@ -49,7 +50,7 @@ class PlaylistWindow extends React.Component {
|
|||
_handleDrop(e, targetCoords) {
|
||||
const top = e.clientY - targetCoords.y;
|
||||
const atIndex = clamp(
|
||||
Math.round((top - 23) / TRACK_HEIGHT),
|
||||
this.props.offset + Math.round((top - 23) / TRACK_HEIGHT),
|
||||
0,
|
||||
this.props.maxTrackIndex + 1
|
||||
);
|
||||
|
|
@ -180,6 +181,7 @@ const mapStateToProps = state => {
|
|||
} = state;
|
||||
|
||||
return {
|
||||
offset: getScrollOffset(state),
|
||||
maxTrackIndex: trackOrder.length - 1,
|
||||
focused,
|
||||
skinPlaylistStyle,
|
||||
|
|
|
|||
|
|
@ -166,9 +166,12 @@ class WindowManager extends React.Component {
|
|||
this.setState(stateDiff);
|
||||
};
|
||||
|
||||
window.addEventListener("mouseup", () => {
|
||||
const removeListeners = () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
});
|
||||
window.removeEventListener("mouseup", removeListeners);
|
||||
};
|
||||
|
||||
window.addEventListener("mouseup", removeListeners);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
}
|
||||
|
||||
|
|
|
|||
14
js/config.js
14
js/config.js
|
|
@ -1,4 +1,5 @@
|
|||
import skin from "../skins/base-2.91.wsz";
|
||||
import llamaAudio from "../mp3/llama-2.91.mp3";
|
||||
|
||||
/* global SENTRY_DSN */
|
||||
|
||||
|
|
@ -12,9 +13,20 @@ if (hash) {
|
|||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility with the old syntax
|
||||
if (config.audioUrl && !config.initialTracks) {
|
||||
config.initialTracks = [{ url: config.audioUrl }];
|
||||
}
|
||||
|
||||
// Turn on the incomplete playlist window
|
||||
export const skinUrl = config.skinUrl === undefined ? skin : config.skinUrl;
|
||||
export const audioUrl = config.audioUrl;
|
||||
export const initialTracks = config.initialTracks || [
|
||||
{
|
||||
metaData: { artist: "DJ Mike Llama", title: "Llama Whippin' Intro" },
|
||||
url: llamaAudio
|
||||
}
|
||||
];
|
||||
|
||||
export const hideAbout = config.hideAbout || false;
|
||||
export const initialState = config.initialState || undefined;
|
||||
export const sentryDsn = SENTRY_DSN;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ export const LOAD_STYLE = {
|
|||
PLAY: "PLAY"
|
||||
};
|
||||
|
||||
export const MEDIA_TAG_REQUEST_STATUS = {
|
||||
INITIALIZED: "INITIALIZED",
|
||||
FAILED: "FAILED",
|
||||
COMPLETE: "COMPLETE",
|
||||
NOT_REQUESTED: "NOT_REQUESTED"
|
||||
};
|
||||
|
||||
export const UTF8_ELLIPSIS = "\u2026";
|
||||
export const CHARACTER_WIDTH = 5;
|
||||
export const PLAYLIST_RESIZE_SEGMENT_WIDTH = 25;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,56 @@
|
|||
import invariant from "invariant";
|
||||
import jsmediatags from "jsmediatags/dist/jsmediatags";
|
||||
|
||||
export function genMediaTags(file) {
|
||||
invariant(
|
||||
file != null,
|
||||
"Attempted to get the tags of media file without passing a file"
|
||||
);
|
||||
// Workaround https://github.com/aadsm/jsmediatags/issues/83
|
||||
if (typeof file === "string" && !/^[a-z]+:\/\//i.test(file)) {
|
||||
file = `${location.protocol}//${location.host}${location.pathname}${file}`;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
jsmediatags.read(file, { onSuccess: resolve, onError: reject });
|
||||
} catch (e) {
|
||||
// Possibly jsmediatags could not find a parser for this file?
|
||||
// Nothing to do.
|
||||
// Consider removing this after https://github.com/aadsm/jsmediatags/issues/83 is resolved.
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function genMediaDuration(url) {
|
||||
invariant(
|
||||
typeof url === "string",
|
||||
"Attempted to get the duration of media file without passing a url"
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: Does this actually stop downloading the file once it's
|
||||
// got the duration?
|
||||
const audio = document.createElement("audio");
|
||||
audio.crossOrigin = "anonymous";
|
||||
const durationChange = () => {
|
||||
resolve(audio.duration);
|
||||
audio.removeEventListener("durationchange", durationChange);
|
||||
audio.url = null;
|
||||
// TODO: Not sure if this really gets cleaned up.
|
||||
};
|
||||
audio.addEventListener("durationchange", durationChange);
|
||||
audio.addEventListener("error", e => {
|
||||
reject(e);
|
||||
});
|
||||
audio.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
export async function genArrayBufferFromFileReference(fileReference) {
|
||||
invariant(
|
||||
fileReference != null,
|
||||
"Attempted to get an ArrayBuffer without assing a fileReference"
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
|
|
@ -28,7 +80,10 @@ export async function genArrayBufferFromUrl(url) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function promptForFileReferences(accept) {
|
||||
export async function promptForFileReferences({
|
||||
accept = null,
|
||||
directory = false
|
||||
}) {
|
||||
return new Promise(resolve => {
|
||||
// Does this represent a memory leak somehow?
|
||||
// Can this fail? Do we ever reject?
|
||||
|
|
@ -36,6 +91,9 @@ export async function promptForFileReferences(accept) {
|
|||
if (accept) fileInput.setAttribute("accept", accept);
|
||||
fileInput.type = "file";
|
||||
fileInput.multiple = true;
|
||||
fileInput.webkitdirectory = directory;
|
||||
fileInput.directory = directory;
|
||||
fileInput.mozdirectory = directory;
|
||||
// Not entirely sure why this is needed, since the input
|
||||
// was just created, but somehow this helps prevent change
|
||||
// events from getting swallowed.
|
||||
|
|
|
|||
45
js/index.js
45
js/index.js
|
|
@ -7,19 +7,35 @@ import visor from "../skins/Vizor1-01.wsz";
|
|||
import xmms from "../skins/XMMS-Turquoise.wsz";
|
||||
import zaxon from "../skins/ZaxonRemake1-0.wsz";
|
||||
import green from "../skins/Green-Dimension-V2.wsz";
|
||||
import llamaAudio from "../mp3/llama-2.91.mp3";
|
||||
import Winamp from "./winamp";
|
||||
|
||||
import {
|
||||
hideAbout,
|
||||
skinUrl,
|
||||
audioUrl,
|
||||
initialTracks,
|
||||
initialState,
|
||||
sentryDsn
|
||||
} from "./config";
|
||||
|
||||
Raven.config(sentryDsn).install();
|
||||
|
||||
// Requires Dropbox's Chooser to be loaded on the page
|
||||
function genAudioFileUrlsFromDropbox() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.Dropbox == null) {
|
||||
reject();
|
||||
}
|
||||
window.Dropbox.choose({
|
||||
success: resolve,
|
||||
error: reject,
|
||||
linkType: "direct",
|
||||
folderselect: false,
|
||||
multiselect: true,
|
||||
extensions: ["video", "audio"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Raven.context(() => {
|
||||
if (hideAbout) {
|
||||
document.getElementsByClassName("about")[0].style.visibility = "hidden";
|
||||
|
|
@ -30,22 +46,11 @@ Raven.context(() => {
|
|||
return;
|
||||
}
|
||||
|
||||
const audio =
|
||||
audioUrl === undefined
|
||||
? {
|
||||
metaData: {
|
||||
artist: "DJ Mike Llama",
|
||||
title: "Llama Whippin' Intro"
|
||||
},
|
||||
url: llamaAudio
|
||||
}
|
||||
: { url: audioUrl };
|
||||
|
||||
const winamp = new Winamp({
|
||||
initialSkin: {
|
||||
url: skinUrl
|
||||
},
|
||||
initialTracks: [audio],
|
||||
initialTracks,
|
||||
avaliableSkins: [
|
||||
{ url: base, name: "<Base Skin>" },
|
||||
{ url: green, name: "Green Dimension V2" },
|
||||
|
|
@ -55,6 +60,18 @@ Raven.context(() => {
|
|||
{ url: xmms, name: "XMMS Turquoise " },
|
||||
{ url: zaxon, name: "Zaxon Remake" }
|
||||
],
|
||||
filePickers: [
|
||||
{
|
||||
contextMenuName: "Dropbox...",
|
||||
filePicker: async () => {
|
||||
const files = await genAudioFileUrlsFromDropbox();
|
||||
return files.map(file => ({
|
||||
url: file.link,
|
||||
defaultName: file.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
],
|
||||
enableHotkeys: true,
|
||||
__initialState: initialState
|
||||
});
|
||||
|
|
|
|||
53
js/loadQueue.js
Normal file
53
js/loadQueue.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import invariant from "invariant";
|
||||
import TinyQueue from "tinyqueue";
|
||||
|
||||
// Push promises onto a queue with a priority.
|
||||
// Run a given number of jobs in parallel
|
||||
// Useful for prioritizing network requests
|
||||
export default class LoadQueue {
|
||||
constructor({ threads }) {
|
||||
// TODO: Consider not running items with zero priority
|
||||
// Priority is a function so that items can change their priority between
|
||||
// when their priority is evaluated.
|
||||
// For example, we might add a track to the playlist and then scroll to/away
|
||||
// from it before it gets processed.
|
||||
this._queue = new TinyQueue([], (a, b) => a.priority() - b.priority());
|
||||
this._avaliableThreads = threads;
|
||||
}
|
||||
|
||||
push(task, priority) {
|
||||
const t = { task, priority };
|
||||
this._queue.push(t);
|
||||
// Wait until the next event loop to pick a task to run. This way, we can
|
||||
// enqueue multiple items in an event loop, and be sure they will be run in
|
||||
// priority order.
|
||||
setTimeout(() => {
|
||||
this._run();
|
||||
}, 0);
|
||||
return () => {
|
||||
// TODO: Could return a boolean representing if the task has already been
|
||||
// kicke off.
|
||||
this._queue = this._queue.filter(t1 => t1 !== t);
|
||||
};
|
||||
}
|
||||
|
||||
_run() {
|
||||
while (this._avaliableThreads > 0) {
|
||||
if (this._queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._avaliableThreads--;
|
||||
const t = this._queue.pop();
|
||||
console.log({ priority: t.priority() });
|
||||
const promise = t.task();
|
||||
invariant(
|
||||
typeof promise.then === "function",
|
||||
`LoadQueue only supports loading Promises. Got ${promise}`
|
||||
);
|
||||
promise.then(() => {
|
||||
this._avaliableThreads++;
|
||||
this._run();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@ export default class ElementSource {
|
|||
}
|
||||
|
||||
// Async for now, for compatibility with BufferAudioSource
|
||||
// TODO: This does not need to be async
|
||||
async loadUrl(url) {
|
||||
this._audio.src = url;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@ import {
|
|||
BUFFER_TRACK,
|
||||
DRAG_SELECTED,
|
||||
SET_MEDIA_TAGS,
|
||||
SET_MEDIA_DURATION
|
||||
SET_MEDIA_DURATION,
|
||||
MEDIA_TAG_REQUEST_INITIALIZED,
|
||||
MEDIA_TAG_REQUEST_FAILED
|
||||
} from "../actionTypes";
|
||||
import { MEDIA_TAG_REQUEST_STATUS } from "../constants";
|
||||
|
||||
import { filenameFromUrl } from "../fileUtils";
|
||||
import { shuffle, moveSelected, mapObject, filterObject } from "../utils";
|
||||
|
|
@ -145,10 +148,12 @@ const playlist = (state = defaultPlaylistState, action) => {
|
|||
tracks: {
|
||||
...state.tracks,
|
||||
[action.id]: {
|
||||
id: action.id,
|
||||
selected: false,
|
||||
defaultName: action.defaultName,
|
||||
duration: null,
|
||||
url: action.url
|
||||
url: action.url,
|
||||
mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.NOT_REQUESTED
|
||||
}
|
||||
},
|
||||
// TODO: This could probably be made to work, but we clear it just to be safe.
|
||||
|
|
@ -172,11 +177,34 @@ const playlist = (state = defaultPlaylistState, action) => {
|
|||
...state.tracks,
|
||||
[action.id]: {
|
||||
...state.tracks[action.id],
|
||||
mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.COMPLETE,
|
||||
title: action.title,
|
||||
artist: action.artist
|
||||
}
|
||||
}
|
||||
};
|
||||
case MEDIA_TAG_REQUEST_INITIALIZED:
|
||||
return {
|
||||
...state,
|
||||
tracks: {
|
||||
...state.tracks,
|
||||
[action.id]: {
|
||||
...state.tracks[action.id],
|
||||
mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.INITIALIZED
|
||||
}
|
||||
}
|
||||
};
|
||||
case MEDIA_TAG_REQUEST_FAILED:
|
||||
return {
|
||||
...state,
|
||||
tracks: {
|
||||
...state.tracks,
|
||||
[action.id]: {
|
||||
...state.tracks[action.id],
|
||||
mediaTagsRequestStatus: MEDIA_TAG_REQUEST_STATUS.FAILED
|
||||
}
|
||||
}
|
||||
};
|
||||
case SET_MEDIA_DURATION:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ describe("playlist reducer", () => {
|
|||
expect(nextState).toEqual({
|
||||
tracks: {
|
||||
100: {
|
||||
id: 100,
|
||||
selected: false,
|
||||
duration: null,
|
||||
defaultName: "My Track Name",
|
||||
mediaTagsRequestStatus: "NOT_REQUESTED",
|
||||
url: "url://some-url"
|
||||
}
|
||||
},
|
||||
|
|
@ -35,8 +37,8 @@ describe("playlist reducer", () => {
|
|||
it("defaults to adding new tracks to the end of the list", () => {
|
||||
const initialState = {
|
||||
tracks: {
|
||||
2: { selected: false },
|
||||
3: { selected: false }
|
||||
2: { id: 2, selected: false },
|
||||
3: { id: 3, selected: false }
|
||||
},
|
||||
trackOrder: [3, 2],
|
||||
lastSelectedIndex: 0
|
||||
|
|
@ -49,11 +51,13 @@ describe("playlist reducer", () => {
|
|||
});
|
||||
expect(nextState).toEqual({
|
||||
tracks: {
|
||||
2: { selected: false },
|
||||
3: { selected: false },
|
||||
2: { id: 2, selected: false },
|
||||
3: { id: 3, selected: false },
|
||||
100: {
|
||||
id: 100,
|
||||
selected: false,
|
||||
duration: null,
|
||||
mediaTagsRequestStatus: "NOT_REQUESTED",
|
||||
defaultName: "My Track Name",
|
||||
url: "url://some-url"
|
||||
}
|
||||
|
|
@ -65,8 +69,8 @@ describe("playlist reducer", () => {
|
|||
it("can handle adding a track at a given index", () => {
|
||||
const initialState = {
|
||||
tracks: {
|
||||
2: { selected: false },
|
||||
3: { selected: false }
|
||||
2: { id: 2, selected: false },
|
||||
3: { id: 3, selected: false }
|
||||
},
|
||||
trackOrder: [3, 2],
|
||||
lastSelectedIndex: 0
|
||||
|
|
@ -80,11 +84,13 @@ describe("playlist reducer", () => {
|
|||
});
|
||||
expect(nextState).toEqual({
|
||||
tracks: {
|
||||
2: { selected: false },
|
||||
3: { selected: false },
|
||||
2: { id: 2, selected: false },
|
||||
3: { id: 3, selected: false },
|
||||
100: {
|
||||
id: 100,
|
||||
selected: false,
|
||||
duration: null,
|
||||
mediaTagsRequestStatus: "NOT_REQUESTED",
|
||||
defaultName: "My Track Name",
|
||||
url: "url://some-url"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const getEqfData = state => {
|
|||
return eqfData;
|
||||
};
|
||||
|
||||
const getTracks = state => state.playlist.tracks;
|
||||
export const getTracks = state => state.playlist.tracks;
|
||||
const getTrackOrder = state => state.playlist.trackOrder;
|
||||
|
||||
export const getOrderedTracks = createSelector(
|
||||
|
|
@ -44,6 +44,7 @@ export const getSelectedTrackObjects = createSelector(
|
|||
tracks => tracks.filter(track => track.selected)
|
||||
);
|
||||
|
||||
// If a duration is `null`, it counts as zero, which seems fine enough.
|
||||
const runningTimeFromTracks = tracks =>
|
||||
tracks.reduce((time, track) => time + Number(track.duration), 0);
|
||||
|
||||
|
|
@ -156,6 +157,19 @@ export const getVisibleTrackIds = createSelector(
|
|||
trackOrder.slice(offset, offset + numberOfVisibleTracks)
|
||||
);
|
||||
|
||||
export const getTrackIsVisibleFunction = createSelector(
|
||||
getVisibleTrackIds,
|
||||
visibleTrackIds => {
|
||||
return id => visibleTrackIds.includes(id);
|
||||
}
|
||||
);
|
||||
|
||||
export const getVisibleTracks = createSelector(
|
||||
getVisibleTrackIds,
|
||||
getTracks,
|
||||
(visibleTrackIds, tracks) => visibleTrackIds.map(id => tracks[id])
|
||||
);
|
||||
|
||||
export const getPlaylist = state => state.playlist;
|
||||
|
||||
export const getDuration = state => {
|
||||
|
|
|
|||
17
js/utils.js
17
js/utils.js
|
|
@ -37,6 +37,9 @@ export const getTimeObj = time => {
|
|||
};
|
||||
|
||||
export const getTimeStr = (time, truncate = true) => {
|
||||
if (time == null) {
|
||||
return "";
|
||||
}
|
||||
const {
|
||||
minutesFirstDigit,
|
||||
minutesSecondDigit,
|
||||
|
|
@ -229,6 +232,18 @@ export const arrayWith = (arr, value) => {
|
|||
|
||||
export const arrayWithout = (arr, value) => {
|
||||
const s = new Set(arr);
|
||||
s["delete"](value);
|
||||
s.delete(value);
|
||||
return Array.from(s);
|
||||
};
|
||||
|
||||
export function debounce(func, delay) {
|
||||
let token;
|
||||
return function(...args) {
|
||||
if (token != null) {
|
||||
clearTimeout(token);
|
||||
}
|
||||
token = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,11 @@ class Winamp {
|
|||
|
||||
render(
|
||||
<Provider store={this.store}>
|
||||
<App media={this.media} container={this.options.container} />
|
||||
<App
|
||||
media={this.media}
|
||||
container={this.options.container}
|
||||
filePickers={this.options.filePickers}
|
||||
/>
|
||||
</Provider>,
|
||||
node
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
"cardinal-spline-js": "^2.3.6",
|
||||
"classnames": "^2.2.5",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"invariant": "^2.2.3",
|
||||
"jest": "^22.0.3",
|
||||
"jsmediatags": "^3.8.1",
|
||||
"jszip": "^3.1.3",
|
||||
|
|
@ -82,6 +83,7 @@
|
|||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"reselect": "^3.0.1",
|
||||
"tinyqueue": "^1.2.3",
|
||||
"webpack": "^3.6.0",
|
||||
"winamp-eqf": "^1.0.0"
|
||||
},
|
||||
|
|
|
|||
31
press.md
31
press.md
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
## News Aggregators
|
||||
|
||||
* [/r/InternetIsBeautiful](https://www.reddit.com/r/InternetIsBeautiful/comments/2lh3ob/winamp_2_preserved_in_html5/)
|
||||
* [/r/InternetIsBeautiful](https://www.reddit.com/r/InternetIsBeautiful/comments/2lh3ob/winamp_2_preserved_in_html5/)
|
||||
* [Hacker News](https://news.ycombinator.com/item?id=16333550) (Feb. 2018)
|
||||
* [Hacker News](https://news.ycombinator.com/item?id=15314629) (Sept. 2017)
|
||||
* [Hacker News](https://news.ycombinator.com/item?id=8565665) (Nov. 2014)
|
||||
|
|
@ -45,6 +45,7 @@
|
|||
## Articles (continued)
|
||||
|
||||
### Russian
|
||||
|
||||
* http://altervision.org/2018/02/winamp-zapustili-v-vashem-brauzere/
|
||||
* http://droider.ru/post/emulyator-dlya-nostalgii-po-winamp-09-02-2018/
|
||||
* http://fornote.net/2018/02/winamp2-js-legendarny-j-winamp-dostupny-j-pryamo-v-brauzere/
|
||||
|
|
@ -62,6 +63,7 @@
|
|||
* https://tech.onliner.by/2018/02/09/winamp-5
|
||||
|
||||
### Spanish
|
||||
|
||||
* http://datainfox.com/2018/02/lanzan-version-web-winamp/
|
||||
* http://html5facil.com/noticias/winamp-web/
|
||||
* http://omicrono.elespanol.com/2014/11/winamp-2-en-html5-es-nostalgia-pura/
|
||||
|
|
@ -81,6 +83,7 @@
|
|||
* https://www.rocambola.com/winamp2-js-la-nueva-version-del-famoso-reproductor-en-formato-web/
|
||||
|
||||
### Chinese
|
||||
|
||||
* http://baijiahao.baidu.com/s?id=1592074295875163282
|
||||
* http://blog.csdn.net/souhugirl/article/details/43907469
|
||||
* http://m.sohu.com/a/221750379_549015
|
||||
|
|
@ -95,6 +98,7 @@
|
|||
* https://www.waerfa.com/winamp2-js
|
||||
|
||||
### English
|
||||
|
||||
* http://entertainment.ie/trending/news/Remember-Winamp-Now-theres-a-web-browser-version-of-it/402141.htm
|
||||
* http://newsvire.com/get-some-media-player-nostalgia-with-this-web-version-of-winamp/
|
||||
* http://osxdaily.com/2018/02/10/run-winamp-web-browser/
|
||||
|
|
@ -103,8 +107,12 @@
|
|||
* https://thenextweb.com/shareables/2018/02/09/bring-nostalgia-browser-winamp-emulator/
|
||||
* https://www.follownews.com/winamp2js-is-a-webbased-version-of-audio-player-winamp-43icg
|
||||
* https://www.gizmodo.com.au/2018/02/someone-remade-winamp-so-it-runs-in-your-browser/
|
||||
* https://changelog.com/news/winamp-29-in-htmljs--PJr
|
||||
* https://www.howtogeek.com/343813/re-live-90s-computing-in-your-browser-right-now/amp/
|
||||
* https://techcaption.com/run-winamp-web-browser/
|
||||
|
||||
### German
|
||||
|
||||
* http://de.engadget.com/2018/02/09/retro-winamp-emulator-fur-deinen-browser/
|
||||
* http://www.chip.de/news/Winamp-ist-zurueck-Kult-Player-ueber-Browser-nutzen_124413881.html
|
||||
* http://www.intro.de/life/mit-winamp-zuruck-in-die-vergangenheit-nostalgische-browser-version-des-media-players
|
||||
|
|
@ -113,6 +121,7 @@
|
|||
* https://www.smartdroid.de/winamp2-js-laesst-den-klassiker-im-browser-wieder-aufleben/
|
||||
|
||||
### Polish
|
||||
|
||||
* https://www.antyradio.pl/Technologia/Internet/Winamp-powrocil-jako-strona-internetowa-17497
|
||||
* http://innpoland.pl/137663,kultowy-winamp-powraca-w-nowej-formie-uderza-tam-gdzie-jest-juz-spotify
|
||||
* http://www.instalki.pl/aktualnosci/software/30055-winamp-przegladarka.html
|
||||
|
|
@ -120,6 +129,7 @@
|
|||
* https://www.dobreprogramy.pl/Kultowy-odtwarzacz-muzyki-Winamp-powrocil-w-przegladarce-internetowej,News,86017.html
|
||||
|
||||
### French
|
||||
|
||||
* http://www.phonandroid.com/winamps-lecteur-multimedia-retour.html
|
||||
* http://www.lemonde.fr/pixels/breve/2014/11/07/winamp-en-html_4520190_4408996.html
|
||||
* https://korben.info/winamp-en-html-javascript.html
|
||||
|
|
@ -128,6 +138,7 @@
|
|||
* https://www.macg.co/logiciels/2018/02/revivez-les-glorieuses-annees-winamp-avec-votre-navigateur-101345
|
||||
|
||||
### Italian
|
||||
|
||||
* https://tech.everyeye.it/notizie/il-ritorno-winamp-storico-player-riappare-in-una-versione-web-320373.html
|
||||
* https://www.hwupgrade.it/news/web/winamp-arriva-su-qualsiasi-browser-web-nostalgici-gioiscono_74100.html
|
||||
* https://www.wired.it/internet/web/2018/02/09/winamp-rinasce-sul-web/
|
||||
|
|
@ -135,47 +146,63 @@
|
|||
* https://www.windowsblogitalia.com/2018/02/ascoltare-la-musica-edge-firefox-chrome-winamp2-js/
|
||||
|
||||
### Thai
|
||||
|
||||
* http://www.flashfly.net/wp/208175
|
||||
* https://www.aripfan.com/bring-winamp-emulator-in-web-browser/
|
||||
* https://www.beartai.com/news/itnews/221530
|
||||
|
||||
### Hungarian
|
||||
* http://hvg.hu/tudomany/20180212_winamp_zenelejatszo_letoltes_bongeszoben
|
||||
|
||||
* http://hvg.hu/tudomany/20180212_winamp_zenelejatszo_letoltes_bongeszoben
|
||||
* http://www.karpatinfo.net/cikk/tudomany/2567147-feltamadt-winamp-90-es-evek-kedvenc-zenelejatszoja-hasznalhatja
|
||||
* https://www.gamestar.hu/hir/winamp-html5-verzio-243839.html
|
||||
|
||||
### Bosnian
|
||||
|
||||
* https://www.b92.net/tehnopolis/vesti.php?yyyy=2018&mm=02&nav_id=1357852
|
||||
* https://www.benchmark.rs/vesti/novi_winamp_emulator_budi_nostalgiju_na_stara_vremena-74941
|
||||
|
||||
### Turkish
|
||||
|
||||
* http://www.bursateknikpc.com/winamp-2nin-html5-de-yazilmis-versiyonu-cikti/
|
||||
* https://www.log.com.tr/winamp-efsanesini-yeniden-kullanima-sunan-site/
|
||||
|
||||
### Japanese
|
||||
|
||||
* http://jp.techcrunch.com/2018/02/10/2018-02-09-whip-the-llamas-ass-with-this-javascript-winamp-emulator/
|
||||
* https://gigazine.net/news/20180213-winamp2-js/
|
||||
* https://www.gizmodo.jp/2014/11/90winamp_2html5.html
|
||||
|
||||
### Portuguese
|
||||
|
||||
* http://abertoatedemadrugada.com/2018/02/winamp-completamente-funcional-em.html
|
||||
* https://canaltech.com.br/musica/desenvolvedor-ressuscita-winamp-em-emulador-com-javascript-108112/
|
||||
|
||||
### Swedish
|
||||
|
||||
* http://gaffa.se/nyhet/125537/klassisk-musikspelare-nu-pa-webben/
|
||||
* https://www.idg.se/2.1085/1.697529/mp3-winamp-webblasare
|
||||
|
||||
### Armenian
|
||||
|
||||
* http://www.armblog.net/2018/02/github-winamp.htm
|
||||
|
||||
### Dutch
|
||||
|
||||
* https://tweakers.net/geek/135087/ontwikkelaar-bouwt-winamp-29-in-javascript.html
|
||||
|
||||
### Romanian
|
||||
|
||||
* https://zonait.tv/va-era-dor-de-winamp/
|
||||
|
||||
### Czech
|
||||
|
||||
* https://www.zive.cz/clanky/zpatky-do-minulosti-winamp-muzete-pouzivat-primo-v-prohlizeci/sc-3-a-191743/default.aspx
|
||||
|
||||
### Croatian
|
||||
|
||||
* http://idesh.net/tech-i-web/internet/winamp-se-vratio-nostalgija-na-najjace/
|
||||
|
||||
### Vietnamese
|
||||
|
||||
* http://vnreview.vn/tin-tuc-xa-hoi-so/-/view_content/content/2421915/song-lai-thap-nien-90-voi-trai-nghiem-nhung-phan-mem-nay-ngay-tren-trinh-duyet
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -3152,6 +3152,12 @@ invariant@^2.0.0, invariant@^2.2.2:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
invariant@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688"
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
invert-kv@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
|
|
@ -6208,6 +6214,10 @@ timers-browserify@^2.0.4:
|
|||
dependencies:
|
||||
setimmediate "^1.0.4"
|
||||
|
||||
tinyqueue@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d"
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue