Progress of file picker

This commit is contained in:
Jordan Eldredge 2019-02-20 09:28:14 -08:00
parent f4dfceface
commit 82d0323f58
10 changed files with 166 additions and 243 deletions

View file

@ -1,59 +0,0 @@
var glob = require("glob");
const path = require("path");
var { exec } = require("child_process");
const _ = require("lodash");
function allSkinPaths() {
return new Promise((resolve, reject) => {
glob(
path.join(
__dirname,
"../../webamp/experiments/automatedScreenshots/skins/",
"**/*.wsz"
),
function(err, files) {
if (err != null) {
reject(err);
return;
}
resolve(files);
}
);
});
}
async function filenamesForSkin(skinPath) {
const paths = await new Promise((resolve, reject) => {
exec(`zipinfo -1 ${skinPath}`, function(error, stdout, stderr) {
if (error != null) {
//reject(error);
//return;
}
resolve(stdout);
});
});
return paths.split("\n");
}
async function allFilenames() {
const skins = await allSkinPaths();
const paths = _.flatten(
await Promise.all(
skins.map(skin => {
return filenamesForSkin(skin);
})
)
);
const fileNames = paths.map(p => path.basename(p).toLowerCase());
const uniqueFileNames = {};
for (fileName of fileNames) {
uniqueFileNames[fileName] =
uniqueFileNames[fileName] == null ? 1 : uniqueFileNames[fileName] + 1;
}
const entries = Object.entries(uniqueFileNames);
const sorted = _.sortBy(entries, ([filename, count]) => count);
console.log(JSON.stringify(sorted, null, 2));
}
allFilenames();

View file

@ -5,6 +5,19 @@ const { getSkinMetadata } = require("./utils");
const client = algoliasearch("HQ9I5Z6IM5", "f5357f4070cdb6ed652d9c3feeede89f");
const index = client.initIndex("Skins");
function tuncate(str, len) {
const overflow = str.length - len;
if (overflow < 0) {
return str;
}
const half = Math.floor((len - 1) / 2);
const start = str.slice(0, half);
const end = str.slice(-half);
return `${start} ########### ${end}`;
}
async function buildSkinIndex(hash) {
const textMetadata = await getSkinMetadata(hash, "extracted-data");
return {
@ -12,7 +25,7 @@ async function buildSkinIndex(hash) {
md5: hash,
fileName: skins[hash].fileName,
emails: textMetadata.emails,
readmeText: textMetadata.raw.slice(0, 1000)
readmeText: tuncate(textMetadata.raw, 4800)
};
}
@ -24,6 +37,12 @@ const indexesPromise = Promise.all(
async function go() {
const indexes = await indexesPromise;
const large = indexes.filter(index => {
return index.readmeText.length > 4790;
});
large.map(l => {
return l.fileName;
});
return new Promise((resolve, reject) => {
index.saveObjects(indexes, function(err, content) {
@ -33,4 +52,4 @@ async function go() {
});
}
go().then(content => console.log(content));
go(); // .then(content => console.log("Updated index for:", content.length));

View file

@ -146,6 +146,10 @@ body.webamp-loaded #webamp {
opacity: 1;
}
.skin img {
box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5);
}
.screenshot {
height: 100%;
opacity: 0;
@ -216,3 +220,17 @@ body.overlay-open .overlay {
margin: 80px auto;
line-height: 1.4em;
}
.readme {
background: white;
}
.readme pre {
padding: 10px;
width: 100%;
height: 100%;
white-space: pre-wrap;
margin: 0;
font-size: 12px;
box-sizing: border-box;
}

View file

@ -1,12 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import WebampComponent from "./WebampComponent";
// import Readme from "./Readme";
import FileExplorer from "./FileExplorer";
import DownloadLink from "./DownloadLink";
import * as Utils from "./utils";
import * as Selectors from "./redux/selectors";
import * as Actions from "./redux/actionCreators";
import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH } from "./constants";
import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH, SKIN_WIDTH } from "./constants";
import { delay } from "rxjs/operators";
import { Subject, combineLatest, timer, fromEvent } from "rxjs";
import Disposable from "./Disposable";
@ -136,7 +136,20 @@ class FocusedSkin extends React.Component {
loaded={this.handleWebampLoaded}
/>
</div>
{/*<Readme skinUrl={Utils.skinUrlFromHash(this.props.hash)} />*/}
{
<FileExplorer
skinUrl={Utils.skinUrlFromHash(this.props.hash)}
style={{
width: "400px",
height: "100%",
top: 0,
transform: `translateX(${
this.props.fileExplorerOpen ? 0 : "-400px"
})`,
transition: "transform 200ms ease-out"
}}
/>
}
</>
)}
<div
@ -215,7 +228,17 @@ class FocusedSkin extends React.Component {
>
Download
</DownloadLink>
{"]"} {"["}
{"] ["}
<a
href={"#"}
onClick={e => {
this.props.openFileExplorer();
e.preventDefault();
}}
>
Readme
</a>
{"] ["}
<a
href={Utils.getAbsolutePermalinkUrlFromHash(this.props.hash)}
onClick={e => {
@ -234,12 +257,16 @@ class FocusedSkin extends React.Component {
const mapStateToProps = state => ({
hash: Selectors.getSelectedSkinHash(state),
initialPosition: Selectors.getSelectedSkinPosition(state)
initialPosition: Selectors.getSelectedSkinPosition(state),
fileExplorerOpen: Selectors.getFileExplorerOpen(state)
});
const mapDispatchToProps = dispatch => ({
selectRelativeSkin(offset) {
dispatch(Actions.selectRelativeSkin(offset));
},
openFileExplorer() {
dispatch(Actions.openFileExplorer());
}
});

View file

@ -1,89 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import * as Actions from "./redux/actionCreators";
class Readme extends React.Component {
_renderFocusedFile() {
if (this.props.focusedFile == null) {
return;
}
const { ext, fileName, content } = this.props.focusedFile;
if (content == null) {
return;
}
switch (ext) {
case "txt":
return (
<textarea
style={{
width: "100%",
height: "300px"
}}
defaultValue={content}
/>
);
case "bmp":
case "cur":
const mimeType = `image/${ext}`;
const url = URL.createObjectURL(
new Blob([content], { type: mimeType })
);
return <img src={url} alt={fileName} />;
default:
return null;
}
}
render() {
return (
<div
style={{
position: "fixed",
width: 500,
left: 0,
height: "100%",
backgroundColor: "white",
overflow: "scroll"
}}
>
<select onChange={e => this.props.selectSkinFile(e.target.value)}>
{this.props.zip &&
Object.keys(this.props.zip.files).map(fileName => (
<option
key={fileName}
value={fileName}
selected={
this.props.focusedFile &&
this.props.focusedFile.fileName === fileName
}
>
{fileName}
</option>
))}
</select>
<div>{this._renderFocusedFile()}</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
zip: state.skinZip,
focusedFile: state.focusedSkinFile
};
}
function mapDispatchToProps(dispatch) {
return {
selectSkinFile(fileName) {
dispatch(Actions.selectSkinFile(fileName));
}
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Readme);

View file

@ -42,3 +42,11 @@ export function requestedAboutPage() {
export function selectRelativeSkin(offset) {
return { type: "SELECT_RELATIVE_SKIN", offset };
}
export function openFileExplorer() {
return { type: "OPEN_FILE_EXPLORER" };
}
export function closeFileExlporer() {
return { type: "CLOSE_FILE_EXPLORER" };
}

View file

@ -3,7 +3,7 @@ import { of, from, empty } from "rxjs";
import * as Actions from "./actionCreators";
import * as Selectors from "./selectors";
import * as Utils from "../utils";
import { filter, switchMap, map } from "rxjs/operators";
import { filter, switchMap, map, ignoreElements } from "rxjs/operators";
import { search } from "../algolia";
const urlChangedEpic = actions =>
@ -18,7 +18,11 @@ const urlChangedEpic = actions =>
if (action.location.pathname.startsWith("/skin/")) {
const segments = action.location.pathname.split("/");
return of(Actions.selectedSkin(segments[2]));
const actions = [Actions.selectedSkin(segments[2])];
if (segments[3] === "files") {
actions.push(Actions.selectSkinFile(segments[4]));
}
return of(...actions);
}
return of(Actions.searchQueryChanged(query || ""));
})
@ -34,11 +38,24 @@ const selectedSkinEpic = actions =>
const JSZip = await import("jszip");
return JSZip.loadAsync(blob);
}),
map(zip => Actions.loadedSkinZip(zip))
switchMap(zip => {
return of(Actions.loadedSkinZip(zip), {
type: "SELECTED_SKIN_README"
});
})
);
})
);
const loadedSkinZipEpic = actions =>
actions.pipe(
filter(action => action.type === "LOADED_SKIN_ZIP"),
switchMap(action => {
// If a file is focused, but not yet loaded, try to load it now?
return empty();
})
);
const focusedSkinFileEpic = (actions, states) =>
actions.pipe(
filter(action => action.type === "SELECTED_SKIN_FILE_TO_FOCUS"),
@ -46,7 +63,7 @@ const focusedSkinFileEpic = (actions, states) =>
// TODO: Ensure this is never called with the wrong zip. Should this live in the "got zip" closure?
const { skinZip } = states.value;
if (skinZip == null) {
// TODO: Should this throw?
// We don't have the skin zip yet. We trust that selectedSkinEpic will call this.
return empty();
}
@ -61,6 +78,41 @@ const focusedSkinFileEpic = (actions, states) =>
})
);
const selectSkinReadmeEpic = (actions, states) =>
actions.pipe(
filter(action => action.type === "SELECTED_SKIN_README"),
switchMap(() => {
// TODO: Ensure this is never called with the wrong zip. Should this live in the "got zip" closure?
const { skinZip } = states.value;
if (skinZip == null) {
return empty();
}
const readmeFileName = Object.keys(skinZip.files).find(filename => {
return (
filename.match(/\.txt$/) &&
![
"genex.txt",
"genexinfo.txt",
"gen_gslyrics.txt",
"region.txt",
"pledit.txt",
"viscolor.txt",
"winampmb.txt",
"gen_ex help.txt",
"mbinner.txt"
// Skinning Updates.txt ?
].some(name => filename.match(new RegExp(name, "i")))
);
});
if (readmeFileName == null) {
return empty();
}
return of(Actions.selectSkinFile(readmeFileName));
})
);
const searchEpic = actions =>
actions.pipe(
filter(action => action.type === "SEARCH_QUERY_CHANGED"),
@ -106,5 +158,7 @@ export default combineEpics(
selectedSkinEpic,
focusedSkinFileEpic,
randomSkinEpic,
selectRelativeSkinEpic
selectRelativeSkinEpic,
selectSkinReadmeEpic,
loadedSkinZipEpic
);

View file

@ -70,13 +70,21 @@ export const getUrl = createSelector(
getActiveContentPage,
getSelectedSkinHash,
getSearchQuery,
(activeContentPage, hash, query) => {
getFileExplorerOpen,
getFocusedSkinFile,
(activeContentPage, hash, query, fileExplorerOpen, focusedSkinFile) => {
if (activeContentPage === ABOUT_PAGE) {
return "/about/";
}
if (hash) {
// TODO: Add a human readable version
return Utils.getPermalinkUrlFromHash(hash);
const skinUrl = Utils.getPermalinkUrlFromHash(hash);
if (fileExplorerOpen && focusedSkinFile) {
return `${skinUrl}files/${encodeURIComponent(
focusedSkinFile.fileName
)}`;
}
return skinUrl;
} else if (query) {
return `/?query=${encodeURIComponent(query)}`;
}
@ -102,3 +110,11 @@ export function getActiveContentPage(state) {
export function getSkins(state) {
return state.skins;
}
export function getFileExplorerOpen(state) {
return state.fileExplorerOpen;
}
export function getFocusedSkinFile(state) {
return state.focusedSkinFile;
}

View file

@ -1,82 +1,9 @@
import { createStore as createReduxStore, applyMiddleware } from "redux";
import { createEpicMiddleware } from "redux-observable";
import skins from "../skins.json";
import * as Selectors from "./selectors";
import rootEpic from "./epics";
import { ABOUT_PAGE } from "../constants";
import reducer from "./reducer";
const defaultState = {
searchQuery: null,
selectedSkinHash: null,
selectedSkinPosition: null,
matchingHashes: null,
skinZip: null,
focusedSkinFile: null,
activeContentPage: null,
skins
};
function reducer(state = defaultState, action) {
switch (action.type) {
case "SELECTED_SKIN":
return {
...state,
selectedSkinHash: action.hash,
selectedSkinPosition: action.position,
skinZip: null
};
case "CLOSE_MODAL":
return {
...state,
selectedSkinHash: null,
selectedSkinPosition: null,
skinZip: null,
activeContentPage: null
};
case "SEARCH_QUERY_CHANGED":
return {
...state,
searchQuery: action.query,
selectedSkinHash: null,
selectedSkinPosition: null
};
case "GOT_NEW_MATCHING_HASHES":
return {
...state,
matchingHashes: action.matchingHashes
};
case "LOADED_SKIN_ZIP":
return {
...state,
skinZip: action.zip
};
case "SELECTED_SKIN_FILE_TO_FOCUS": {
return {
...state,
focusedSkinFile: {
content: null,
ext: action.ext,
fileName: action.fileName
}
};
}
case "GOT_FOCUSED_SKIN_FILE":
return {
...state,
focusedSkinFile: {
...state.focusedSkinFile,
content: action.content
}
};
case "REQUESTED_ABOUT_PAGE":
return {
...state,
activeContentPage: ABOUT_PAGE
};
default:
return state;
}
}
export function createStore() {
const epicMiddleware = createEpicMiddleware();

16
todo.md
View file

@ -1,13 +1,16 @@
# TODO
- Add archive.org link?
- Sort skins by... something? (Twitter likes?)
- Scroll is not locked in overlay mode on ios
- Fix about page on mobile
- Fix animation for about page overlay fade in
- "No results" page
## Blocking
- Figure out how to get prerendering to work for Facebook
- Waiting on Netlify
# Future
- Add archive.org link?
- Scroll is not locked in overlay mode on ios
- Fix animation for about page overlay fade in
- "No results" page
- Setup Sentry
- Restore focus when leaving the modal
- Add keyboard navigation to table
@ -32,7 +35,6 @@
## Requires a server
- Add an opengraph preview image URL to skin permalinks
- Drag in a skin to upload!
- We should detect if we have it or not
- Allow users to add tags/author info