Initial commit - Split from Lesbos theme
This commit is contained in:
commit
147cc3e0fa
|
@ -0,0 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "discord-css-injector"
|
||||
authors = ["videogame hacker <half-kh-hacker@hackery.site>"]
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
include_dir = "0.6.1"
|
||||
minifier = "0.0.41"
|
||||
once_cell = "1.8.0"
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
serde_json = "1.0.64"
|
||||
tokio = { version = "1.8.0", features = ["full"] }
|
||||
warp = "0.3.1"
|
||||
|
||||
# Why is opening a browser on Windows so annoying???
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["shellapi"] }
|
|
@ -0,0 +1,38 @@
|
|||
The Charlotte Public License version 0.1
|
||||
|
||||
Copyright 2021, Charlotte Som (the "Author" henceforth).
|
||||
|
||||
This license gives everyone permission to examine, modify, and use this software
|
||||
and the associated documentation (the "Inator"), without patent obstacles, while protecting
|
||||
the Author and any contributors (the "Composers") from liability.
|
||||
|
||||
Each Composer permits you to examine, modify, utilize, and distribute the Inator
|
||||
where it would otherwise infringe upon that Composer's copyright or any patent claims that
|
||||
they hold.
|
||||
|
||||
No contributor may revoke this license, but the Author may choose to release the Inator
|
||||
(including the contributed works of any other Composer) under a different license.
|
||||
|
||||
You may not use the Inator to accrue revenue without explicit permission from the Author.
|
||||
|
||||
You may not use the Inator to do Malevolence. If you are
|
||||
notified that you have committed a Malevolence instrumented by the Inator, your license is
|
||||
terminated unless you take all practical steps to comply within a reasonable timeframe.
|
||||
|
||||
The definition of Malevolence is at the discretion of the Author. It may include, but is not
|
||||
limited to:
|
||||
|
||||
- The promotion of bigotry, including: sexism, transphobia, homophobia, ableism, or the
|
||||
perpetuation of racial oppression.
|
||||
- Causing a detriment to public health.
|
||||
- Instigating political, economic, or corporeal violence.
|
||||
- Entrenching an empire.
|
||||
- Where applicable, use of the Inator without the informed consent of a second party who may
|
||||
object to its use.
|
||||
|
||||
The Inator is provided without any warranty, "as-is". No Composer is liable for any damages
|
||||
related to the Inator.
|
||||
|
||||
In order to receive this license, you must agree to the terms set out in this document.
|
||||
This license, authorial attribution, and copyright notice must be distributed with
|
||||
any copies or large portions of the Inator.
|
|
@ -0,0 +1,18 @@
|
|||
# discord-css-injector
|
||||
|
||||
This is a theme injector that doesn't require patching the Discord client.
|
||||
As a bonus, this means that you can easily use themes in Discord's web application,
|
||||
but the downside is that you have to apply your theme manually every time you start Discord.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
$ cargo run
|
||||
[...]
|
||||
Listening on: http://127.0.0.1:12345/
|
||||
```
|
||||
|
||||
1. A browser should open at the given URL
|
||||
2. Choose the theme you want to apply
|
||||
3. Hit 'Copy JS'
|
||||
4. Paste into Discord's DevTools console (you can hit Ctrl + Shift + I to open this)
|
|
@ -0,0 +1,20 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use crate::themes::Theme;
|
||||
|
||||
const INJECTOR_TEMPLATE: &str = include_str!("./panel/injector.template.js");
|
||||
|
||||
pub fn render_injector(current_addr: &SocketAddr, theme: &Theme) -> String {
|
||||
let stylesheet_urls: Vec<String> = theme
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| format!("http://{}/styles/{}/{}", current_addr, &theme.slug, f))
|
||||
.collect();
|
||||
|
||||
let stylesheet_urls =
|
||||
serde_json::to_string(&stylesheet_urls).expect("Couldn't encode stylesheet URLs");
|
||||
|
||||
let injector_src = INJECTOR_TEMPLATE.replace("/* STYLESHEET_URLS */null", &stylesheet_urls);
|
||||
|
||||
minifier::js::minify(&injector_src)
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
// Incantation to not allocate a Windows console when compiled in release mode
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::{collections::HashMap, net::SocketAddr, time::Duration};
|
||||
|
||||
use include_dir::{include_dir, Dir};
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use warp::{
|
||||
http::HeaderValue,
|
||||
hyper::HeaderMap,
|
||||
reply::{json, Response},
|
||||
Filter, Rejection, Reply,
|
||||
};
|
||||
|
||||
mod injector;
|
||||
mod open_url;
|
||||
mod themes;
|
||||
|
||||
use crate::themes::{read_theme_from_dir, Theme};
|
||||
|
||||
static CURRENT_ADDRESS: OnceCell<SocketAddr> = OnceCell::new();
|
||||
static THEMES: Lazy<HashMap<String, Theme>> = Lazy::new(|| {
|
||||
let mut themes = HashMap::new();
|
||||
|
||||
for entry in std::fs::read_dir("styles").unwrap() {
|
||||
if entry.is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = entry.unwrap().path();
|
||||
if path.is_dir() {
|
||||
if let Some(theme) = read_theme_from_dir(path) {
|
||||
themes.insert(theme.slug.clone(), theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
themes
|
||||
});
|
||||
|
||||
fn injector(theme_name: String) -> String {
|
||||
if let Some(theme) = THEMES.get(&theme_name) {
|
||||
let current_addr = CURRENT_ADDRESS.get().expect("Couldn't get current address");
|
||||
injector::render_injector(current_addr, theme)
|
||||
} else {
|
||||
"// Unknown theme, sorry!".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn themes() -> impl Reply {
|
||||
let themes: &HashMap<_, _> = &THEMES;
|
||||
json(themes)
|
||||
}
|
||||
|
||||
fn shutdown() -> String {
|
||||
let _ = std::thread::spawn(|| {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
std::process::exit(0)
|
||||
});
|
||||
|
||||
"Goodbye!".to_string()
|
||||
}
|
||||
|
||||
const PANEL_DIR: Dir = include_dir!("./src/panel");
|
||||
|
||||
async fn serve_panel_file(path: warp::path::Tail) -> Result<Response, Rejection> {
|
||||
let mut file_name = path.as_str().to_string();
|
||||
if ("/".to_string() + &file_name).ends_with("/") {
|
||||
file_name.push_str("index.html");
|
||||
}
|
||||
|
||||
if let Some(file) = PANEL_DIR.get_file(&file_name) {
|
||||
let (mut head, body) = Response::new(file.contents().into()).into_parts();
|
||||
|
||||
let extension = file_name.rfind('.').map(|idx| &file_name[idx..]);
|
||||
let content_type = match extension {
|
||||
Some(".html") => "text/html; charset=utf-8",
|
||||
Some(".css") => "text/css; charset=utf-8",
|
||||
Some(".js") => "application/javascript; charset=utf-8",
|
||||
Some(".txt") => "text/plain; charset=utf-8",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
|
||||
head.headers
|
||||
.insert("Content-Type", HeaderValue::from_str(content_type).unwrap());
|
||||
|
||||
return Ok(Response::from_parts(head, body));
|
||||
}
|
||||
|
||||
Err(warp::reject::not_found())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let styles_route = {
|
||||
let mut static_headers = HeaderMap::new();
|
||||
for (h, v) in [
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Pragma", "no-cache"),
|
||||
("Cache-Control", "no-cache"),
|
||||
] {
|
||||
static_headers.insert(h, HeaderValue::from_static(v));
|
||||
}
|
||||
|
||||
warp::path("styles")
|
||||
.and(warp::fs::dir("styles"))
|
||||
.with(warp::reply::with::headers(static_headers))
|
||||
};
|
||||
|
||||
let themes_list_route = warp::path("themes.json").map(themes);
|
||||
let injector_route = warp::path!("theme" / String / "injector.js").map(injector);
|
||||
let panel_route = warp::get()
|
||||
.and(warp::path::tail())
|
||||
.and_then(serve_panel_file);
|
||||
let shutdown_route = warp::post().and(warp::path("shutdown")).map(shutdown);
|
||||
|
||||
let routes = styles_route
|
||||
.or(injector_route)
|
||||
.or(themes_list_route)
|
||||
.or(panel_route)
|
||||
.or(shutdown_route);
|
||||
|
||||
let (addr, listener) = warp::serve(routes).bind_ephemeral(([127, 0, 0, 1], 0));
|
||||
println!("Listening on: http://{}/", &addr);
|
||||
|
||||
open_url::open(&format!("http://{}/", addr));
|
||||
|
||||
CURRENT_ADDRESS
|
||||
.set(addr)
|
||||
.expect("Could not set current address");
|
||||
|
||||
listener.await;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
#[cfg(target_os = "windows")]
|
||||
pub fn open(url: &str) {
|
||||
extern crate winapi;
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::ptr;
|
||||
use winapi::um::shellapi::ShellExecuteA;
|
||||
|
||||
unsafe {
|
||||
let open_str = CString::new("open").unwrap();
|
||||
let url_str = CString::new(url.to_string().replace("\n", "%0A")).unwrap();
|
||||
|
||||
ShellExecuteA(
|
||||
ptr::null_mut(),
|
||||
open_str.as_ptr(),
|
||||
url_str.as_ptr(),
|
||||
ptr::null(),
|
||||
ptr::null(),
|
||||
winapi::um::winuser::SW_SHOWNORMAL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn open(url: &str) {
|
||||
let _ = std::process::Command::new("open")
|
||||
.arg(url.to_string())
|
||||
.output();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn open(url: &str) {
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(url.to_string())
|
||||
.output();
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>discord themes - local injector</title>
|
||||
<link rel="stylesheet" href="/panel.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>discord themes</h1>
|
||||
<em>without patching your client :)</em>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>Themes</h2>
|
||||
|
||||
<p>choose a theme, hit the 'Copy JS' button, and paste it into the Discord console:</p>
|
||||
|
||||
<ul id="themes"></ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<form id="shutdown-form" method="POST" action="/shutdown">
|
||||
<h2>Exit</h2>
|
||||
<p>To exit the theming provider, you can hit this button.</p>
|
||||
|
||||
<input type="submit" value="Exit">
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
hey, uh, discord say that pasting untrusted things into the console
|
||||
can give malicious actors access to your account.
|
||||
|
||||
this is <strong>entirely true</strong>. be careful!
|
||||
</p>
|
||||
|
||||
<p>i pinkie swear that I'm not trying to hack you, though</p>
|
||||
|
||||
<p>a project by <a href="https://som.codes">charlotte som</a>.</p>
|
||||
</footer>
|
||||
|
||||
<script src="/panel.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
const STYLESHEET_URLS = /* STYLESHEET_URLS */null;
|
||||
|
||||
const denormalisedRegex = /(.*)-[A-Za-z0-9\-\_]{6}$/;
|
||||
const setupNormalization = () => {
|
||||
const addNormalizedClassNames = (element) => {
|
||||
const normalizedClasses = [];
|
||||
|
||||
if (!element.classList)
|
||||
return;
|
||||
|
||||
for (const className of element.classList) {
|
||||
const matches = className.match(denormalisedRegex);
|
||||
if (matches) {
|
||||
if (matches[0].startsWith("hljs-"))
|
||||
continue;
|
||||
|
||||
const normalized = matches[1];
|
||||
normalizedClasses.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
normalizedClasses.forEach(it => element.classList.add(it));
|
||||
};
|
||||
|
||||
for (const element of document.querySelectorAll("*"))
|
||||
addNormalizedClassNames(element);
|
||||
|
||||
const obs = new MutationObserver((mutations, observer) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "childList") {
|
||||
for (const node of mutation.addedNodes) {
|
||||
addNormalizedClassNames(node);
|
||||
|
||||
for (const subNode of node.querySelectorAll("*"))
|
||||
addNormalizedClassNames(subNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (mutation.type === "attributes") {
|
||||
// If we don't check this, we get into some infinite loop shenanigans
|
||||
let alreadyNormalized = false;
|
||||
for (const className of mutation.target.classList) {
|
||||
if (!className.match(denormalisedRegex)) {
|
||||
alreadyNormalized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyNormalized)
|
||||
addNormalizedClassNames(mutation.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
obs.observe(
|
||||
document.documentElement,
|
||||
{
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class"]
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const injectSingleCSS = async (url) => {
|
||||
const text = await fetch(url).then(r => r.text());
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.innerText = text;
|
||||
|
||||
document.head.appendChild(style);
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
if (window.stylesheetsToInject) {
|
||||
window.stylesheetsToInject = STYLESHEET_URLS;
|
||||
window.reloadCSS();
|
||||
return;
|
||||
}
|
||||
|
||||
setupNormalization();
|
||||
|
||||
let tags = [];
|
||||
const applyCSS = async (urls) => {
|
||||
const promises = [];
|
||||
for (const url of urls)
|
||||
promises.push(injectSingleCSS(url));
|
||||
|
||||
tags = [];
|
||||
for (const t of (await Promise.all(promises)))
|
||||
tags.push(t);
|
||||
};
|
||||
|
||||
window.stylesheetsToInject = STYLESHEET_URLS;
|
||||
await applyCSS(window.stylesheetsToInject);
|
||||
window.reloadCSS = async () => {
|
||||
for (const tag of tags) {
|
||||
tag.remove();
|
||||
}
|
||||
|
||||
await applyCSS(window.stylesheetsToInject);
|
||||
};
|
||||
};
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
main();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", main);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
:root {
|
||||
--bg: #292841 !important;
|
||||
--accent: #b4a4f8;
|
||||
--accent-dark: #8971f4;
|
||||
--success: #ffd1dc;
|
||||
--fg: #fbfbfb !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
body {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
main, footer {
|
||||
margin: 0 auto;
|
||||
max-width: 60ch;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-bottom: 1em;
|
||||
margin-bottom: 1em;
|
||||
border-bottom: 1px solid var(--fg);
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-top: 1em;
|
||||
margin-top: 1em;
|
||||
border-top: 1px solid var(--fg);
|
||||
}
|
||||
|
||||
button, input[type=submit] {
|
||||
background-color: var(--accent);
|
||||
border: var(--accent-dark);
|
||||
border-radius: 6px;
|
||||
color: var(--fg);
|
||||
padding: 0.25em 0.5em;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
|
||||
user-select: none;
|
||||
border: 1px solid rgba(0,0,0,0);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
|
||||
transition: color .15s ease-in-out, background-color .15s ease-in-out;
|
||||
}
|
||||
|
||||
button.copied {
|
||||
background-color: var(--success);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#shutdown-form {
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
#shutdown-form > input[type=submit] {
|
||||
background-color: red;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
const main = async () => {
|
||||
const themesContainer = document.querySelector("#themes");
|
||||
const themes = await fetch("/themes.json").then(r => r.json());
|
||||
|
||||
for (const slug in themes) {
|
||||
const theme = themes[slug];
|
||||
|
||||
// TODO
|
||||
const listItem = document.createElement("li");
|
||||
|
||||
const themeName = document.createElement("strong");
|
||||
themeName.textContent = theme.name;
|
||||
|
||||
const themeDescription = document.createElement("em");
|
||||
themeDescription.textContent = theme.description;
|
||||
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.textContent = "Copy JS";
|
||||
copyButton.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
|
||||
(async () => {
|
||||
const injectorJS = await fetch(`/theme/${slug}/injector.js`).then(r => r.text());
|
||||
await navigator.clipboard.writeText(injectorJS)
|
||||
|
||||
copyButton.textContent = "Copied!";
|
||||
copyButton.classList.add("copied");
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = "Copy JS";
|
||||
copyButton.classList.remove("copied");
|
||||
}, 2500);
|
||||
})();
|
||||
});
|
||||
|
||||
listItem.appendChild(themeName);
|
||||
listItem.appendChild(themeDescription);
|
||||
listItem.appendChild(copyButton);
|
||||
|
||||
themesContainer.appendChild(listItem);
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
main();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", main);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Theme {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub slug: String,
|
||||
#[serde(skip)]
|
||||
pub files: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn read_theme_from_dir(dir: PathBuf) -> Option<Theme> {
|
||||
if let Some((slug, mut theme)) = dir
|
||||
.file_name()
|
||||
.map(|dir_name| dir_name.to_string_lossy().to_string())
|
||||
.and_then(|slug| {
|
||||
std::fs::read_to_string(dir.join("theme.json"))
|
||||
.map(|theme_json| (slug, theme_json))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|(slug, theme_json)| {
|
||||
serde_json::from_str::<Theme>(&theme_json)
|
||||
.ok()
|
||||
.map(|theme| (slug, theme))
|
||||
})
|
||||
{
|
||||
theme.slug = slug.clone();
|
||||
|
||||
let files: Vec<_> = std::fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|r| r.is_ok())
|
||||
.map(|r| r.unwrap())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.filter(|n| n.ends_with(".css"))
|
||||
.collect();
|
||||
|
||||
theme.files = files;
|
||||
|
||||
return Some(theme);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
Loading…
Reference in New Issue