This commit is contained in:
Michael Zhang 2024-07-27 16:28:33 -05:00
commit cf9cf06d2b
16 changed files with 670 additions and 0 deletions

175
.gitignore vendored Normal file
View file

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"json.schemas": [
{
"fileMatch": ["manifest.json"],
"url": "https://json.schemastore.org/chrome-manifest"
}
]
}

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# tweaks
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.1.20. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

17
biome.json Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

BIN
bun.lockb Normal file

Binary file not shown.

1
index.ts Normal file
View file

@ -0,0 +1 @@
console.log("Hello via Bun!");

11
manifest.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "OsuTweaks",
"version": "0.1.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["*://osu.ppy.sh/*"],
"js": ["src/script.ts"]
}
]
}

23
package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "tweaks",
"module": "index.ts",
"type": "module",
"scripts": {
"dev": "vite"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@types/bun": "latest",
"@types/lodash.isequal": "^4.5.8",
"@types/webextension-polyfill": "^0.10.7",
"vite-plugin-web-extension": "^4.1.6"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"lodash.isequal": "^4.5.0",
"vite": "^5.3.5",
"webextension-polyfill": "^0.12.0"
}
}

170
src/colors.ts Normal file
View file

@ -0,0 +1,170 @@
// From https://gist.githubusercontent.com/mjackson/5311256/raw/132d6d1f39bf422e03c8ab86b5329d6f4dfbc383/color-conversion-algorithms.js
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
export function rgbToHsl(r: number, g: number, b: number) {
(r /= 255), (g /= 255), (b /= 255);
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h,
s,
l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, l];
}
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number l The lightness
* @return Array The RGB representation
*/
export function hslToRgb(h, s, l) {
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255];
}
/**
* Converts an RGB color value to HSV. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and v in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSV representation
*/
export function rgbToHsv(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h,
s,
v = max;
var d = max - min;
s = max == 0 ? 0 : d / max;
if (max == min) {
h = 0; // achromatic
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, v];
}
/**
* Converts an HSV color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number v The value
* @return Array The RGB representation
*/
export function hsvToRgb(h, s, v) {
var r, g, b;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
(r = v), (g = t), (b = p);
break;
case 1:
(r = q), (g = v), (b = p);
break;
case 2:
(r = p), (g = v), (b = t);
break;
case 3:
(r = p), (g = q), (b = v);
break;
case 4:
(r = t), (g = p), (b = v);
break;
case 5:
(r = v), (g = p), (b = q);
break;
}
return [r * 255, g * 255, b * 255];
}

63
src/dom.ts Normal file
View file

@ -0,0 +1,63 @@
import isEqual from "lodash.isequal";
const REFRESH_RATE = 1000;
export async function waitForElementToExist(
selector: string,
): Promise<Element> {
let interval: Timer;
return new Promise((resolve) => {
interval = setInterval(() => {
const thing = document.querySelector(selector);
if (thing) {
clearInterval(interval);
resolve(thing);
}
}, REFRESH_RATE);
});
}
function onDocumentReady(func: () => void) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", func);
} else {
func();
}
}
export interface OsuPath {
pathname: string;
hash: string;
}
export function addPageChangeHandler(func: (path: OsuPath) => void) {
onDocumentReady(() => {
console.log("Page loaded.");
// detects which page we're on
// detect page change, ripped from osuplus
// this is really ugly but whatever
// spent an hour trying to jack some addEventListener calls with no success
let currentPath: OsuPath = {
pathname: location.pathname,
hash: location.hash,
};
setInterval(() => {
const newPath = {
pathname: location.pathname,
hash: location.hash,
};
if (!isEqual(currentPath, newPath)) {
func(newPath);
currentPath = newPath;
}
}, REFRESH_RATE);
// the very first time the browser is opened, handle the page visit
func(currentPath);
});
console.log("Isntallled event listener");
}

104
src/pages/beatmap.ts Normal file
View file

@ -0,0 +1,104 @@
import { hslToRgb, rgbToHsl } from "../colors";
import { waitForElementToExist, type OsuPath } from "../dom";
import { parseCssColor } from "../utils";
const MODE_HANDLER = /(?<mode>osu|taiko|fruits|mania)\/(?<bid>\d+)/;
export async function beatmapPageHandler(m: RegExpMatchArray, path: OsuPath) {
const m2 = (path.hash ?? "").match(MODE_HANDLER);
const modeString = m2[1];
// get the beatmapset data
// TODO: error checking?
const dataEl = document.getElementById("json-beatmapset");
const beatmapsetData = JSON.parse(dataEl?.textContent ?? "{}");
// remove existing difficulty name indicator
waitForElementToExist(".beatmapset-header__diff-name").then(
(diffNameIndicator) => {
diffNameIndicator.parentNode?.removeChild(diffNameIndicator);
},
);
// const starRatingIndicator = await waitForElementToExist(
// ".beatmapset-header__star-difficulty",
// );
// starRatingIndicator.parentNode.removeChild(starRatingIndicator);
// put the names into the bubbles
const beatmapMap = new Map<string, unknown>();
for (const beatmap of [
...beatmapsetData.beatmaps,
...beatmapsetData.converts,
])
beatmapMap.set(JSON.stringify([beatmap.id, beatmap.mode]), beatmap);
const beatmapPicker = await waitForElementToExist(
".beatmapset-beatmap-picker",
);
const beatmapPickerChildren: HTMLAnchorElement[] = Array.from(
beatmapPicker.children,
);
for (const child of beatmapPickerChildren) {
if (!child.href) continue;
const targetBeatmapId = Number.parseInt(child.href.split("/").at(-1));
const correspondingBeatmap = beatmapMap.get(
JSON.stringify([targetBeatmapId, modeString]),
);
// create the elements and add it to screen
const icon = child.children[0];
const container = document.createElement("div");
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "0";
container.style.transform = "translateZ(1px)";
const diffNameContainer = document.createElement("div");
const diffNameNode = document.createTextNode(correspondingBeatmap.version);
diffNameContainer.appendChild(diffNameNode);
const colorString = icon.style.getPropertyValue("--diff");
const color = parseCssColor(colorString);
const hsl = rgbToHsl(...color);
// make sure the colors aren't too dim
if (hsl[2] < 0.5) hsl[2] = 0.5;
hsl[2] *= 1.5;
if (hsl[2] > 1) hsl[2] = 1;
let [r, g, b] = hslToRgb(...hsl);
diffNameContainer.style.lineHeight = "0.75";
child.style.color = `rgb(${r}, ${g}, ${b})`;
diffNameContainer.style.textShadow = "0 1px 3px rgba(0,0,0,.75)";
container.appendChild(diffNameContainer);
const starRating = correspondingBeatmap.difficulty_rating.toLocaleString(
"en-US",
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: false,
},
);
const infoContainer = document.createElement("small");
const infoNode = document.createTextNode(`${starRating}`);
infoContainer.style.color = `rgb(${r}, ${g}, ${b})`;
infoContainer.style.textShadow = "0 1px 3px rgba(0,0,0,.75)";
infoContainer.style.display = "block";
infoContainer.style.fontSize = "75%";
infoContainer.appendChild(infoNode);
child.style.opacity = "1";
child.style.width = "auto";
child.style.height = "auto";
child.style.display = "flex";
child.style.alignItems = "center";
child.style.paddingLeft = "8px";
child.style.paddingRight = "12px";
child.style.gap = "6px";
container.appendChild(infoContainer);
child.replaceChildren(icon, container);
}
}

8
src/pages/user.ts Normal file
View file

@ -0,0 +1,8 @@
export async function userPageHandler(m, path) {
console.log("User page");
let currentUser = await wait(
() => unsafeWindow.currentUser,
(c) => !!c,
);
console.log(currentUser);
}

27
src/script.ts Normal file
View file

@ -0,0 +1,27 @@
import { addPageChangeHandler, type OsuPath } from "./dom";
import { beatmapPageHandler } from "./pages/beatmap";
import { userPageHandler } from "./pages/user";
console.log("OsuTweaks Extension");
const pathHandlers: [
RegExp,
(m: RegExpMatchArray, _: OsuPath) => Promise<void>,
][] = [
[/\/users\/(\d+)(?<mode>\/(osu|taiko|fruits|mania).+)?/, userPageHandler],
[/\/beatmapsets\/(?<id>\d+).?/, beatmapPageHandler],
];
addPageChangeHandler((path) => {
console.log("Changed to page", path);
const { pathname } = path;
for (const [regex, callback] of pathHandlers) {
const m = pathname.match(regex);
if (m !== null) {
// a match was found
callback(m, path);
break;
}
}
});

12
src/utils.ts Normal file
View file

@ -0,0 +1,12 @@
// Color parsing function
// minified, original is from https://stackoverflow.com/a/68580275
export function parseCssColor(str: string): [number, number, number] {
const div = document.createElement("div");
document.body.appendChild(div);
div.style.color = str;
const res = getComputedStyle(div)
.color.match(/[\.\d]+/g)
.map(Number);
div.remove();
return res;
}

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

9
vite.config.ts Normal file
View file

@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import webExtension from "vite-plugin-web-extension";
export default defineConfig({
plugins: [webExtension({
disableAutoLaunch: true,
browser: process.env.TARGET ?? "firefox"
})],
});