mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 18:25:30 +00:00
342 lines
9.7 KiB
JavaScript
342 lines
9.7 KiB
JavaScript
import SKIN_SPRITES from "./skinSprites";
|
|
import regionParser from "./regionParser";
|
|
import { LETTERS, DEFAULT_SKIN } from "./constants";
|
|
import { parseViscolors, parseIni, getFileExtension, objectMap } from "./utils";
|
|
|
|
const shallowMerge = objs =>
|
|
objs.reduce((prev, img) => Object.assign(prev, img), {});
|
|
|
|
const CURSORS = [
|
|
"CLOSE",
|
|
"EQCLOSE",
|
|
"EQNORMAL",
|
|
"EQSLID",
|
|
"EQTITLE",
|
|
"MAINMENU",
|
|
"MMENU",
|
|
"MIN",
|
|
"NORMAL",
|
|
"PCLOSE",
|
|
"PNORMAL",
|
|
"POSBAR",
|
|
"PSIZE",
|
|
"PTBAR",
|
|
"PVSCROLL",
|
|
"PWINBUT",
|
|
"PWSNORM",
|
|
"PWSSIZE",
|
|
"SONGNAME",
|
|
"TITLEBAR",
|
|
"VOLBAL",
|
|
"WINBUT",
|
|
"WSNORMAL",
|
|
"WSPOSBAR",
|
|
/*
|
|
* > There are usually 4 more cursors in the skins: volbar.cur, wsclose.cur,
|
|
* > wswinbut.cur, wsmin.cur, but they are never used, at least in the last
|
|
* > versions of winamp, so there's no need of including them. The cursors
|
|
* > shown when the mouse is over the app-buttons are the same in normal and
|
|
* > winshade mode, except for the main menu button. You can make animated
|
|
* > cursors, but you have to name them with the extension .cur (animated
|
|
* > cursors are usually .ani files).
|
|
*
|
|
* -- Skinners Atlas
|
|
*
|
|
* "VOLBAR",
|
|
* "WSCLOSE",
|
|
* "WSWINBUT",
|
|
* "WSMIN",
|
|
*
|
|
*/
|
|
];
|
|
|
|
const _genImgFromBlob = async blob =>
|
|
new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
// Schedule cleanup of object url?
|
|
// Maybe on next tick, or with requestidlecallback
|
|
resolve(img);
|
|
};
|
|
img.onerror = () => reject("Failed to decode image");
|
|
img.src = URL.createObjectURL(blob);
|
|
});
|
|
|
|
const genImgFromBlob = async blob => {
|
|
if (window.createImageBitmap) {
|
|
try {
|
|
// Use this faster native browser API if available.
|
|
return await window.createImageBitmap(blob);
|
|
} catch (e) {
|
|
console.warn(
|
|
"Encountered an error with createImageBitmap. Falling back to Image approach."
|
|
);
|
|
// There are some bugs in the new API. In case something goes wrong, we call fall back.
|
|
return _genImgFromBlob(blob);
|
|
}
|
|
}
|
|
return _genImgFromBlob(blob);
|
|
};
|
|
|
|
async function genFileFromZip(zip, fileName, ext, mode) {
|
|
const regex = new RegExp(`^(.*/)?${fileName}(\.${ext})?$`, "i");
|
|
const files = zip.file(regex);
|
|
if (!files.length) {
|
|
return null;
|
|
}
|
|
// Return a promise (awaitable).
|
|
return {
|
|
contents: await files[0].async(mode),
|
|
name: files[0].name,
|
|
};
|
|
}
|
|
|
|
function getSpriteUrisFromImg(img, sprites) {
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
return sprites.reduce((images, sprite) => {
|
|
canvas.height = sprite.height;
|
|
canvas.width = sprite.width;
|
|
|
|
context.drawImage(img, -sprite.x, -sprite.y);
|
|
const image = canvas.toDataURL();
|
|
images[sprite.name] = image;
|
|
return images;
|
|
}, {});
|
|
}
|
|
|
|
async function genImgFromFilename(zip, fileName) {
|
|
// Winamp only supports .bmp images, but WACUP set a precidence of supporting
|
|
// .png as well to reduce size. Since we care about size as well, we follow
|
|
// suit. Our default skin uses .png to save 14kb.
|
|
const file = await genFileFromZip(zip, fileName, "(png|bmp)", "blob");
|
|
if (!file) {
|
|
return null;
|
|
}
|
|
|
|
const mimeType = `image/${getFileExtension(file.name) || "*"}`;
|
|
// The spec for createImageBitmap() says the browser should try to sniff the
|
|
// mime type, but it looks like Firefox does not. So we specify it here
|
|
// explicitly.
|
|
const typedBlob = new Blob([file.contents], { type: mimeType });
|
|
return genImgFromBlob(typedBlob);
|
|
}
|
|
|
|
async function genSpriteUrisFromFilename(zip, fileName) {
|
|
const img = await genImgFromFilename(zip, fileName);
|
|
if (img == null) {
|
|
return {};
|
|
}
|
|
return getSpriteUrisFromImg(img, SKIN_SPRITES[fileName]);
|
|
}
|
|
|
|
async function getCursorFromFilename(zip, fileName) {
|
|
const file = await genFileFromZip(zip, fileName, "CUR", "base64");
|
|
return file && `data:image/x-win-bitmap;base64,${file.contents}`;
|
|
}
|
|
|
|
async function genPlaylistStyle(zip) {
|
|
const pledit = await genFileFromZip(zip, "PLEDIT", "txt", "text");
|
|
const data = pledit && parseIni(pledit.contents).text;
|
|
if (!data) {
|
|
// Corrupt or missing PLEDIT.txt file.
|
|
return DEFAULT_SKIN.playlistStyle;
|
|
}
|
|
|
|
// Winamp seems to permit colors that contain too many characters.
|
|
// For compatibility with existing skins, we normalize them here.
|
|
["normal", "current", "normalbg", "selectedbg", "mbFG", "mbBG"].forEach(
|
|
colorKey => {
|
|
let color = data[colorKey];
|
|
if (!color) {
|
|
return;
|
|
}
|
|
if (color[0] !== "#") {
|
|
color = `#${color}`;
|
|
}
|
|
data[colorKey] = color.slice(0, 7);
|
|
}
|
|
);
|
|
|
|
return { ...DEFAULT_SKIN.playlistStyle, ...data };
|
|
}
|
|
|
|
async function genVizColors(zip) {
|
|
const viscolor = await genFileFromZip(zip, "VISCOLOR", "txt", "text");
|
|
return viscolor ? parseViscolors(viscolor.contents) : DEFAULT_SKIN.colors;
|
|
}
|
|
|
|
async function genImages(zip) {
|
|
const imageObjs = await Promise.all(
|
|
Object.keys(SKIN_SPRITES).map(async fileName =>
|
|
genSpriteUrisFromFilename(zip, fileName)
|
|
)
|
|
);
|
|
// Merge all the objects into a single object. Tests assert that sprite keys are unique.
|
|
return shallowMerge(imageObjs);
|
|
}
|
|
async function genCursors(zip) {
|
|
const cursorObjs = await Promise.all(
|
|
CURSORS.map(async cursorName => ({
|
|
[cursorName]: await getCursorFromFilename(zip, cursorName),
|
|
}))
|
|
);
|
|
return shallowMerge(cursorObjs);
|
|
}
|
|
|
|
async function genRegion(zip) {
|
|
const region = await genFileFromZip(zip, "REGION", "txt", "text");
|
|
return region ? regionParser(region.contents) : {};
|
|
}
|
|
|
|
async function genGenTextSprites(zip) {
|
|
const img = await genImgFromFilename(zip, "GEN");
|
|
if (img == null) {
|
|
return null;
|
|
}
|
|
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
context.drawImage(img, 0, 0);
|
|
|
|
const getLetters = (y, prefix) => {
|
|
const getColorAt = x => context.getImageData(x, y, 1, 1).data.join(",");
|
|
|
|
let x = 1;
|
|
const backgroundColor = getColorAt(0);
|
|
|
|
const height = 7;
|
|
return LETTERS.map(letter => {
|
|
let nextBackground = x;
|
|
while (
|
|
getColorAt(nextBackground) !== backgroundColor &&
|
|
nextBackground < canvas.width
|
|
) {
|
|
nextBackground++;
|
|
}
|
|
const width = nextBackground - x;
|
|
const name = `${prefix}_${letter}`;
|
|
const sprite = { x, y, height, width, name };
|
|
x = nextBackground + 1;
|
|
return sprite;
|
|
});
|
|
};
|
|
|
|
const letterWidths = {};
|
|
const sprites = [
|
|
...getLetters(88, "GEN_TEXT_SELECTED"),
|
|
...getLetters(96, "GEN_TEXT"),
|
|
];
|
|
sprites.forEach(sprite => {
|
|
letterWidths[sprite.name] = sprite.width;
|
|
});
|
|
return [letterWidths, getSpriteUrisFromImg(img, sprites)];
|
|
}
|
|
|
|
async function genGenExColors(zip) {
|
|
const img = await genImgFromFilename(zip, "GENEX");
|
|
if (img == null) {
|
|
return null;
|
|
}
|
|
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
context.drawImage(img, 0, 0);
|
|
|
|
const getColorAt = x =>
|
|
`rgb(${context
|
|
.getImageData(x, 0, 1, 1)
|
|
// Discard the alpha channel
|
|
.data.slice(0, 3)
|
|
.join(",")})`;
|
|
|
|
const colors = {
|
|
// (1) x=48: item background (background to edits, listviews, etc.)
|
|
itemBackground: 48,
|
|
// (2) x=50: item foreground (text colour of edits, listviews, etc.)
|
|
itemForeground: 50,
|
|
// (3) x=52: window background (used to set the bg color for the dialog)
|
|
windowBackground: 52,
|
|
// (4) x=54: button text colour
|
|
buttonText: 54,
|
|
// (5) x=56: window text colour
|
|
windowText: 56,
|
|
// (6) x=58: colour of dividers and sunken borders
|
|
divider: 58,
|
|
// (7) x=60: selection colour for entries inside playlists (nothing else yet)
|
|
playlistSelection: 60,
|
|
// (8) x=62: listview header background colour
|
|
listHeaderBackground: 62,
|
|
// (9) x=64: listview header text colour
|
|
listHeaderText: 64,
|
|
// (10) x=66: listview header frame top and left colour
|
|
listHeaderFrameTopAndLeft: 66,
|
|
// (11) x=68: listview header frame bottom and right colour
|
|
listHeaderFrameBottomAndRight: 68,
|
|
// (12) x=70: listview header frame colour, when pressed
|
|
listHeaderFramePressed: 70,
|
|
// (13) x=72: listview header dead area colour
|
|
listHeaderDeadArea: 72,
|
|
// (14) x=74: scrollbar colour #1
|
|
scrollbarOne: 74,
|
|
// (15) x=76: scrollbar colour #2
|
|
scrollbarTwo: 76,
|
|
// (16) x=78: pressed scrollbar colour #1
|
|
pressedScrollbarOne: 78,
|
|
// (17) x=80: pressed scrollbar colour #2
|
|
pressedScrollbarTwo: 80,
|
|
// (18) x=82: scrollbar dead area colour
|
|
scrollbarDeadArea: 82,
|
|
// (19) x=84 List view text colour highlighted
|
|
listTextHighlighted: 84,
|
|
// (20) x=86 List view background colour highlighted
|
|
listTextHighlightedBackground: 86,
|
|
// (21) x=88 List view text colour selected
|
|
listTextSelected: 88,
|
|
// (22) x=90 List view background colour selected
|
|
listTextSelectedBackground: 90,
|
|
};
|
|
|
|
return objectMap(colors, getColorAt);
|
|
}
|
|
|
|
// A promise that, given an array buffer returns a skin style object
|
|
async function skinParser(zipFileBuffer, JSZip) {
|
|
const zip = await JSZip.loadAsync(zipFileBuffer);
|
|
|
|
const [
|
|
colors,
|
|
playlistStyle,
|
|
images,
|
|
cursors,
|
|
region,
|
|
genTextSprites,
|
|
genExColors,
|
|
] = await Promise.all([
|
|
genVizColors(zip),
|
|
genPlaylistStyle(zip),
|
|
genImages(zip),
|
|
genCursors(zip),
|
|
genRegion(zip),
|
|
genGenTextSprites(zip),
|
|
genGenExColors(zip),
|
|
]);
|
|
|
|
const [genLetterWidths, genTextImages] = genTextSprites || [null, {}];
|
|
|
|
return {
|
|
colors,
|
|
playlistStyle,
|
|
images: { ...images, ...genTextImages },
|
|
genLetterWidths,
|
|
cursors,
|
|
region,
|
|
genExColors,
|
|
};
|
|
}
|
|
|
|
export default skinParser;
|