diff --git a/scripts/allFileNames.js b/scripts/allFileNames.js
deleted file mode 100644
index 056b896a..00000000
--- a/scripts/allFileNames.js
+++ /dev/null
@@ -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();
diff --git a/scripts/createSearchIndex.js b/scripts/createSearchIndex.js
index ec435a16..1c2f1364 100644
--- a/scripts/createSearchIndex.js
+++ b/scripts/createSearchIndex.js
@@ -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));
diff --git a/src/App.css b/src/App.css
index 4c34d484..46717544 100644
--- a/src/App.css
+++ b/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;
+}
diff --git a/src/FocusedSkin.js b/src/FocusedSkin.js
index 95fd6db0..ef0e2a36 100644
--- a/src/FocusedSkin.js
+++ b/src/FocusedSkin.js
@@ -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}
/>
- {/**/}
+ {
+
+ }
>
)}
Download
- {"]"} {"["}
+ {"] ["}
+
{
+ this.props.openFileExplorer();
+ e.preventDefault();
+ }}
+ >
+ Readme
+
+ {"] ["}
{
@@ -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());
}
});
diff --git a/src/Readme.js b/src/Readme.js
deleted file mode 100644
index 0df1c9a5..00000000
--- a/src/Readme.js
+++ /dev/null
@@ -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 (
-
- );
- case "bmp":
- case "cur":
- const mimeType = `image/${ext}`;
- const url = URL.createObjectURL(
- new Blob([content], { type: mimeType })
- );
- return
;
- default:
- return null;
- }
- }
-
- render() {
- return (
-
-
-
{this._renderFocusedFile()}
-
- );
- }
-}
-
-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);
diff --git a/src/redux/actionCreators.js b/src/redux/actionCreators.js
index dfb5fe0a..ed97f605 100644
--- a/src/redux/actionCreators.js
+++ b/src/redux/actionCreators.js
@@ -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" };
+}
diff --git a/src/redux/epics.js b/src/redux/epics.js
index 41114360..80e15839 100644
--- a/src/redux/epics.js
+++ b/src/redux/epics.js
@@ -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
);
diff --git a/src/redux/selectors.js b/src/redux/selectors.js
index ac6a6269..bf91b7c1 100644
--- a/src/redux/selectors.js
+++ b/src/redux/selectors.js
@@ -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;
+}
diff --git a/src/redux/store.js b/src/redux/store.js
index 17fbfe72..37f60db9 100644
--- a/src/redux/store.js
+++ b/src/redux/store.js
@@ -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();
diff --git a/todo.md b/todo.md
index 1aa8b66a..cc0afefd 100644
--- a/todo.md
+++ b/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