mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 18:17:38 +00:00
Progress of file picker
This commit is contained in:
parent
f4dfceface
commit
82d0323f58
10 changed files with 166 additions and 243 deletions
|
|
@ -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();
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
18
src/App.css
18
src/App.css
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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" };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
16
todo.md
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue