Compare commits

..

No commits in common. "main" and "theo/remove-potential-danger" have entirely different histories.

13 changed files with 3356 additions and 53 deletions

View file

@ -186,10 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 Theo Browne
Copyright and license for original Material Theme project can be found at
https://github.com/Dramaga11/vsc-material-theme/blob/main/LICENSE
Copyright 2024 Theo Browne
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,9 +1,7 @@
# Material Theme (But I won't sue you)
> **Note:** The original Material Theme has been removed from the marketplace due to [distributing malware through their extensions](https://news.ycombinator.com/item?id=43181591). This fork has been thoroughly audited and is completely safe to use. I have personally audited every line, and removed a TON of unnecessary stuff to be sure. The VS Code team is auditing it as well just to be extra safe.
So, uh, the guy who made the VS Code Material Theme is threatening everyone who uses it in their products. He [seems to have forgotten it was originally licensed under the Apache License, 2.0.](https://github.com/Dramaga11/vsc-material-theme/blob/main/LICENSE). He wiped the commit history to make it look like it was always his weird fake license. Check out my [blog post for more info](https://t3.gg/blog/post/equinusocio).
So, uh, the guy who made the VS Code Material Theme is threatening everyone who uses it in their products. He [seems to have forgotten it was originally licensed under the Apache License, 2.0.](https://github.com/Dramaga11/vsc-material-theme/blob/main/LICENSE). He wiped the commit history to make it look like it was always his weird fake license.
What he has done is fraudulent and shameful. I have created this fork to maintain the original license and keep the project alive.
@ -88,6 +86,20 @@ Learn how to customize every part of this theme by using Visual Studio Code API.
}
```
## Official Portings
You can find all the official portings and resources [here](https://github.com/material-theme/vsc-material-theme/discussions/1279).
## Want to use the legacy version?
If you're looking for the deprecated Community Material Theme [you can find it here](https://github.com/material-theme/vsc-material-theme/discussions/1278). This version has been deprecated and removed from the official marketplace.
## Contributors
This project exists thanks to all the people who contribute. [[Contribute]](CONTRIBUTING.md).
<p align="center"><a href="http://www.apache.org/licenses/LICENSE-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-5E81AC.svg?style=flat-square"/></a></p>
## Attribution
The code in this project was [previously hosted at this url](https://github.com/material-theme/vsc-material-theme), but the original author has wiped all history of it, making it incredibly hard to credit him and the other original contributors. Full credit has been preserved in the git history here to our best ability.

2975
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "vsc-material-theme-but-i-wont-sue-you",
"displayName": "Material Theme (But I Won't Sue You)",
"description": "A Apache-2 licensed fork of Material Theme with no threats of legal action",
"version": "35.0.2",
"version": "35.0.0",
"publisher": "t3dotgg",
"license": "Apache-2.0",
"author": "Theo",
@ -23,15 +23,17 @@
"vscode": "^1.51.0"
},
"scripts": {
"build": "run-s cleanup build:ts build:generate-themes",
"build": "run-s cleanup build:ts build:generate-themes build:ui",
"cleanup": "rimraf build && rimraf dist",
"lint": "eslint .",
"build:ui": "node dist/scripts/ui/index.js",
"build:generate-themes": "node dist/scripts/generator/index.js",
"build:ts": "tsc -p ./tsconfig.json && ncp dist/src/ build && ncp material-theme.config.json build",
"postinstall": "node ./node_modules/vscode/bin/install && tsc -p tsconfig.json"
},
"categories": [
"Themes"
"Themes",
"Other"
],
"keywords": [
"VSCode",
@ -173,7 +175,7 @@
"fs-extra": "9.0.1",
"ncp": "2.0.0",
"npm-run-all": "4.1.5",
"rimraf": "3.0.2",
"standard-version": "9.5.0",
"typescript": "4.1.3",
"vscode": "1.1.37"
},

17
scripts/ui/index.ts Normal file
View file

@ -0,0 +1,17 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import {BUILD_FOLDER_PATH} from '../../src/env';
const UI_FOLDER_BUILD_PATH = path.join(BUILD_FOLDER_PATH, 'ui');
const run = async (): Promise<void> => {
try {
await fs.mkdirp(UI_FOLDER_BUILD_PATH);
} catch (error) {
console.error('ERROR build:ui:', error);
process.exit(1);
}
};
void run();

View file

@ -17,10 +17,16 @@ type PackageJSON = {
};
};
type InstallationType = {
firstInstall: boolean;
update: boolean;
};
export interface IExtensionManager {
init: (context: ExtensionContext) => Promise<void>;
getPackageJSON: () => PackageJSON;
getConfig: () => MaterialThemeConfig;
getInstallationType: () => Record<string, unknown>;
updateConfig: (config: Partial<MaterialThemeConfig>) => Promise<void>;
VERSION_KEY: string;
}
@ -30,6 +36,7 @@ class ExtensionManager implements IExtensionManager {
return 'vsc-material-theme.version';
}
installationType: InstallationType;
private readonly configFileUri: Uri;
private readonly userConfigFileUri: Uri;
private configJSON: MaterialThemeConfig;
@ -48,6 +55,10 @@ class ExtensionManager implements IExtensionManager {
return this.configJSON;
}
getInstallationType(): InstallationType {
return this.installationType;
}
async updateConfig(config: Partial<MaterialThemeConfig>): Promise<void> {
const newConfig = {...this.configJSON, ...config};
await workspace.fs.writeFile(this.configFileUri, Buffer.from(JSON.stringify(newConfig), 'utf-8'));
@ -56,27 +67,62 @@ class ExtensionManager implements IExtensionManager {
async init(context: ExtensionContext): Promise<void> {
try {
const packageJSON = this.getPackageJSON();
const userConfig = await this.getUserConfig();
const mementoStateVersion = context.globalState.get(this.VERSION_KEY);
const themeNeverUsed = mementoStateVersion === undefined || typeof mementoStateVersion !== 'string';
this.installationType = {
update: userConfig && this.isVersionUpdate(userConfig, packageJSON),
firstInstall: !userConfig && themeNeverUsed
};
// Theme not used before across devices
if (themeNeverUsed) {
await context.globalState.update(this.VERSION_KEY, packageJSON.version);
}
// Load configuration
const configBuffer = await workspace.fs.readFile(this.configFileUri);
const configContent = Buffer.from(configBuffer).toString('utf8');
this.configJSON = JSON.parse(configContent) as MaterialThemeConfig;
// Update version in user config
const userConfigUpdate = {...this.configJSON, changelog: {lastversion: packageJSON.version}};
await workspace.fs.writeFile(
this.userConfigFileUri,
Buffer.from(JSON.stringify(userConfigUpdate), 'utf-8')
);
// Store version in global state
await context.globalState.update(this.VERSION_KEY, packageJSON.version);
} catch (error) {
this.configJSON = {accentsProperties: {}, accents: {}};
await window
.showErrorMessage(`Material Theme: there was an error while loading the configuration. Please retry or open an issue: ${String(error)}`);
}
}
private isVersionUpdate(userConfig: MaterialThemeConfig, packageJSON: PackageJSON): boolean {
const splitVersion = (input: string): {major: number; minor: number; patch: number} => {
const [major, minor, patch] = input.split('.').map(i => parseInt(i, 10));
return {major, minor, patch};
};
const versionCurrent = splitVersion(packageJSON.version);
const versionOld = splitVersion(userConfig.changelog.lastversion);
const update = (
versionCurrent.major > versionOld.major ||
versionCurrent.minor > versionOld.minor ||
versionCurrent.patch > versionOld.patch
);
return update;
}
private async getUserConfig(): Promise<MaterialThemeConfig | undefined> {
try {
const configBuffer = await workspace.fs.readFile(this.userConfigFileUri);
const configContent = Buffer.from(configBuffer).toString('utf8');
return JSON.parse(configContent) as MaterialThemeConfig;
} catch {}
}
}
export const extensionManager = new ExtensionManager();

45
src/webviews/Settings.ts Normal file
View file

@ -0,0 +1,45 @@
// WIP
// Import {WebviewController} from './Webview';
// import {
// workspace as Workspace
// } from 'vscode';
// import {ISettingsBootstrap} from './interfaces';
// import {getCustomSettings} from '../helpers/settings';
// import {getDefaultValues} from '../helpers/fs';
// export class SettingsWebview extends WebviewController<ISettingsBootstrap> {
// get filename(): string {
// return 'settings.html';
// }
// get id(): string {
// return 'materialTheme.settings';
// }
// get title(): string {
// return 'Material Theme Settings';
// }
// /**
// * This will be called by the WebviewController when init the view
// * passing as `window.bootstrap` to the view.
// */
// getBootstrap(): ISettingsBootstrap {
// return {
// config: getCustomSettings(),
// defaults: getDefaultValues(),
// scope: 'user',
// scopes: this.getAvailableScopes()
// };
// }
// private getAvailableScopes(): Array<['user' | 'workspace', string]> {
// const scopes: Array<['user' | 'workspace', string]> = [['user', 'User']];
// return scopes
// .concat(
// Workspace.workspaceFolders?.length ?
// ['workspace', 'Workspace'] :
// []
// );
// }
// }

161
src/webviews/Webview.ts Normal file
View file

@ -0,0 +1,161 @@
import * as path from 'path';
import {
workspace as Workspace,
Disposable,
ExtensionContext,
WebviewPanel,
ViewColumn,
window,
WebviewPanelOnDidChangeViewStateEvent,
Uri
} from 'vscode';
import {Invalidates, Message} from './interfaces';
export abstract class WebviewController<TBootstrap> extends Disposable {
private panel: WebviewPanel | undefined;
private disposablePanel: Disposable | undefined;
private invalidateOnVisible: Invalidates;
private readonly context: ExtensionContext;
constructor(context: ExtensionContext) {
// Applying dispose callback for our disposable function
super(() => this.dispose());
this.context = context;
}
dispose(): void {
if (this.disposablePanel) {
this.disposablePanel.dispose();
}
}
async show(): Promise<void> {
const html = await this.getHtml();
// If panel already opened just reveal
if (this.panel !== undefined) {
// Replace placeholders in html content for assets and adding configurations as `window.bootstrap`
const fullHtml = this.replaceInPanel(html);
this.panel.webview.html = fullHtml;
return this.panel.reveal(ViewColumn.Active);
}
this.panel = window.createWebviewPanel(
this.id,
this.title,
ViewColumn.Active,
{
retainContextWhenHidden: true,
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true
}
);
// Applying listeners
this.disposablePanel = Disposable.from(
this.panel,
this.panel.onDidDispose(this.onPanelDisposed, this),
this.panel.onDidChangeViewState(this.onViewStateChanged, this),
this.panel.webview.onDidReceiveMessage(this.onMessageReceived, this)
);
// Replace placeholders in html content for assets and adding configurations as `window.bootstrap`
const fullHtml = this.replaceInPanel(html);
this.panel.webview.html = fullHtml;
}
protected onMessageReceived(event: Message): void {
if (event === null) {
return;
}
console.log(`WebviewEditor.onMessageReceived: type=${event.type}, data=${JSON.stringify(event)}`);
switch (event.type) {
case 'saveSettings':
// TODO: update settings
break;
default:
break;
}
}
private replaceInPanel(html: string): string {
// Replace placeholders in html content for assets and adding configurations as `window.bootstrap`
const fullHtml = html
.replace(/{{root}}/g, this.panel.webview.asWebviewUri(Uri.file(this.context.asAbsolutePath('./build'))).toString())
.replace(/{{cspSource}}/g, this.panel.webview.cspSource)
.replace('\'{{bootstrap}}\'', JSON.stringify(this.getBootstrap()));
return fullHtml;
}
private async getHtml(): Promise<string> {
const doc = await Workspace
.openTextDocument(this.context.asAbsolutePath(path.join('build/ui', this.filename)));
return doc.getText();
}
// Private async postMessage(message: Message, invalidates: Invalidates = 'all'): Promise<boolean> {
// if (this.panel === undefined) {
// return false;
// }
// const result = await this.panel.webview.postMessage(message);
// // If post was ok, update invalidateOnVisible if different than default
// if (!result && this.invalidateOnVisible !== 'all') {
// this.invalidateOnVisible = invalidates;
// }
// return result;
// }
// Private async postUpdatedConfiguration(): Promise<boolean> {
// // Post full raw configuration
// return this.postMessage({
// type: 'settingsChanged',
// config: getCustomSettings()
// } as ISettingsChangedMessage, 'config');
// }
private onPanelDisposed(): void {
if (this.disposablePanel) {
this.disposablePanel.dispose();
}
this.panel = undefined;
}
private async onViewStateChanged(event: WebviewPanelOnDidChangeViewStateEvent): Promise<void> {
console.log('WebviewEditor.onViewStateChanged', event.webviewPanel.visible);
if (!this.invalidateOnVisible || !event.webviewPanel.visible) {
return;
}
// Update the view since it can be outdated
const invalidContext = this.invalidateOnVisible;
this.invalidateOnVisible = undefined;
switch (invalidContext) {
case 'config':
// Post the new configuration to the view
// return this.postUpdatedConfiguration();
return;
default:
return this.show();
}
}
abstract get filename(): string;
abstract get id(): string;
abstract get title(): string;
abstract getBootstrap(): TBootstrap;
}

View file

@ -0,0 +1,33 @@
export interface ISettingsChangedMessage {
type: 'settingsChanged';
config: Record<string, unknown>;
}
export interface ISaveSettingsMessage {
type: 'saveSettings';
changes: {
[key: string]: any;
};
removes: string[];
scope: 'user' | 'workspace';
uri: string;
}
export type Message = ISaveSettingsMessage | ISettingsChangedMessage;
export type Invalidates = 'all' | 'config' | undefined;
export interface IBootstrap {
config: Record<string, unknown>;
}
export interface ISettingsBootstrap extends IBootstrap {
scope: 'user' | 'workspace';
scopes: Array<['user' | 'workspace', string]>;
defaults: Record<string, unknown>;
}
declare global {
interface Window {
bootstrap: IBootstrap | ISettingsBootstrap | Record<string, unknown>;
}
}

View file

@ -0,0 +1,20 @@
import {ISettingsBootstrap} from '../../interfaces';
// Import accentsSelector from './lib/accents-selector';
const run = (): void => {
bind();
const {config, defaults} = window.bootstrap as ISettingsBootstrap;
// AccentsSelector('[data-setting="accentSelector"]', defaults.accents, config.accent);
console.log(defaults);
console.log(config);
};
const bind = (): void => {
document.querySelector('#fixIconsCTA').addEventListener('click', () => {
console.log('Test click');
});
};
run();

View file

@ -0,0 +1,26 @@
// Import {IAccents} from '../../../../interfaces/idefaults';
const templateSingleAccent = (accentName: string, accentColor: string): string => {
const dashAccentName = accentName.toLowerCase().replace(/ /gi, '-');
return `
<label for="${dashAccentName}" data-color="${accentColor}">${accentName}</label>
<input type="radio" name="accents" id="${dashAccentName}" value="${dashAccentName}" />
`;
};
export default (containerSelector: string, accentsObject: Record<string, string>, currentAccent: string): void => {
const container = document.querySelector(containerSelector);
for (const accentKey of Object.keys(accentsObject)) {
const el = document.createElement('div');
el.innerHTML = templateSingleAccent(accentKey, accentsObject[accentKey]);
if (accentKey === currentAccent) {
el.setAttribute('selected', 'true');
el.querySelector('input').setAttribute('checked', 'checked');
}
container.appendChild(el);
}
};

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Material Theme Settings Editor</title>
<link rel="stylesheet" href="{{root}}/ui/settings/style.css">
<link rel="stylesheet" href="https://unpkg.com/@native-elements/native-elements/dist/native-elements.css">
</head>
<body>
<header>
<h1>Material Theme Settings Editor (preview)</h1>
</header>
<main>
<button ne-button tabindex="2" autofocus>Button</button>
<div class="SettingsContainer">
<div>
<label>Fix file icons</label>
<input id="fixIconsCTA" type="submit" value="Fix">
</div>
<div class="AccentsRadioContainer" data-setting="accentSelector">
<!-- Populated by js -->
</div>
</div>
</main>
<input value="Example input">
<script type="text/javascript">
window.bootstrap = '{{bootstrap}}';
</script>
<script type="text/javascript" src="{{root}}/ui/settings.js"></script>
</body>
</html>

View file

@ -0,0 +1,4 @@
.AccentsRadioContainer > div {
display: inline-block;
margin-right: 10px;
}