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