global: implement the new theme engine (#2196)

* mobile: theme

* theme: add theme engine

* mobile: migrate app colors to new theme engine

* mobile: fixed some colors

* mobile: fix colors

* mobile: store theme info in store

* theme: `ColorsType` -> `Variants`

* theme: use explicit return type for `useThemeColors`

* theme: add `backdrop` color

* mobile: `const colors` -> `const {colors}

* theme: add default pitch-black theme

* mobile: manage theme state via theme-engine

* mobile: add theme scopes

* mobile: commit

* mobile: fix button width on applock screen

* mobile: fix typings

* mobile: fix theme definition

* web: add partial support for custom themes

only context menus & popups are left.

* theme: add dialog & sheet scopes

* global: sync with master branch and make everything work again

* mobile: fix theme-engine usage in editor & app

* mobile: fix colors

* mobile: fix colors

* mobile: cleanup

* mobile: fix status bar color incorrect on entering foreground

* mobile: fix dark color scheme

* web: move emotion theme provider to @notesnook/theme

* editor: add support for theme enging

* web: adjust hover & focus colors on list item

* mobile: migrate share ext to theme engine

* mobile: fix editor theme provider

* clipper: add support for the new theme engine

* mobile: fix statusbar color on switch from bg

* misc: fix build

* mobile: fix build

* misc: fix colors

* mobile: fix theme colors

* mobile: fix bottom padding

* server: add theme server

* theme: add previewColors

* server: support themes query pagination

* mobile: add client from theme server

* server: reset cache on sync repo

* server: fix types

* server: show ip & port on start server

* server: theme updates

* web: finalize new theme engine on web

* editor: fix build

* global: fix @emotion/react version to 11.11.1

* editor: update katex patch

* web: fix imports

* global: fix @trpc/* versions

* global: a huge set of changes

1. get rid of ThemeVariant. All variants can now be accessed anywhere.
2. remove unnecessary button variants
3. make buttons more responsive
4. implement themes server

* web: add support for theme search and theme switching

* global: update lockfiles

* mobile: fix error

* theme: use vite-plugin-react to start theme server

* web: add support for auto updating themes

* mobile: update theme selector

* mobile: update theme if new verison available

* theme: add `isomorphic-fetch` package

* global: update lockfiles

* web: add theme details dialog

* setup: add scope for themes server in bootstrap script

* web: add production server url

* web: update lockfile

* web: update lockfile

* mobile: remove `react-native-blob-util`

* web: add support for endless scrolling in themes

* web: bring back dark/light mode option in settings

* web: fix colors in places

* theme: add selected variant

* global: use single typescript version across the projects

* web: fix sort & group options not having submenus

* web: apply selected variant where appropriate

* ui: use unique id for all menu items

* config: add ui scope for commits

* theme: export button variant creation fn

* web: fix only 1 theme showing in theme selector

* web: fix navigation item hover & other colors

* mobile: update theme

* editor: fix toolbar group alignments

* editor: set theme provider at app level

* theme: use scope name to get current scope

* mobile: fix color usage in message card

* theme: remove caching

* editor: bring back icons in table menus

* theme: use zustand to manage theme engine state

* web: fix login/signup theming

* mobile: fix webpack build

* misc: remove ThemeProvider usage

* editor: adjust theming and styling of editor toolbar

* mobile: refactor

* editor: fix toolbar group padding everywhere

* web: fix settings sidebar is not scrollable

* web: add loading indicator for themes loading

* mobile: fix warning

* mobile: fix ui issues

* web: fix Loader errors on build

* theme: add getPreviewColors & validateTheme

* theme: fix theme validation

* mobile: load theme from file

* mobile: fix share extension crash

* mobile: rename state

* theme: add sourceURL property

* theme: refactor theme-engine

* web: add support for loading theme from file

* web: improve button hover interaction

* mobile: fix floating button color

* mobile: update theme

* mobile: fix border radius of context menu

* mobile: set sheet overlay color to theme backdrop

* mobile: set sidemenu backdrop to theme backdrop

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
Ammar Ahmed
2023-08-01 12:07:21 +05:00
committed by GitHub
parent ba72a92a8a
commit 622294b807
439 changed files with 16910 additions and 8986 deletions

3
servers/themes/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
themes-metadata.json
notesnook-themes

20
servers/themes/global.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "vite/client";

2726
servers/themes/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "@notesnook/themes-server",
"version": "1.0.0",
"description": "A simple rest api for notesnook themes",
"private": "true",
"main": "src/index.ts",
"scripts": {
"start": "vite-node -w src/server.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@orama/orama": "^1.0.8",
"@trpc/server": "10.31.0",
"async-mutex": "^0.4.0",
"cors": "^2.8.5",
"util": "^0.12.5",
"zod": "^3.21.4"
},
"devDependencies": {
"@notesnook/theme": "file:../../packages/theme",
"@types/cors": "^2.8.13",
"@vitejs/plugin-react": "^4.0.3",
"react": "17.0.2",
"vite-node": "^0.33.0"
}
}

63
servers/themes/src/api.ts Normal file
View File

@@ -0,0 +1,63 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { z } from "zod";
import { InstallsCounter } from "./constants";
import { findTheme, getThemes } from "./orama";
import { syncThemes } from "./sync";
import { publicProcedure, router } from "./trpc";
import { THEME_COMPATIBILITY_VERSION } from "@notesnook/theme";
import { ThemeQuerySchema } from "./schemas";
export const ThemesAPI = router({
themes: publicProcedure.input(ThemeQuerySchema).query(async ({ input }) => {
return getThemes(input);
}),
installTheme: publicProcedure
.input(
z.object({
id: z.string(),
userId: z.string().optional(),
compatibilityVersion: z.number().default(THEME_COMPATIBILITY_VERSION)
})
)
.query(async ({ input: { compatibilityVersion, id, userId } }) => {
const theme = await findTheme(id, compatibilityVersion);
if (!theme) return;
if (userId) await InstallsCounter.increment(theme.id, userId);
return theme;
}),
updateTheme: publicProcedure
.input(
z.object({
id: z.string(),
version: z.number(),
compatibilityVersion: z.number()
})
)
.query(async ({ input: { id, version, compatibilityVersion } }) => {
const theme = await findTheme(id, compatibilityVersion);
if (theme && theme.version !== version) return theme;
}),
sync: publicProcedure.query(() => {
syncThemes();
return true;
})
});

View File

@@ -0,0 +1,29 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import { Counter } from "./counter";
export const THEMES_REPO_URL = `https://github.com/streetwriters/notesnook-themes.git`;
export const THEME_REPO_DIR_NAME = "notesnook-themes";
export const THEME_METADATA_JSON = path.join(__dirname, "themes-metadata.json");
export const THEME_REPO_DIR_PATH = path.resolve(
path.join(__dirname, "..", THEME_REPO_DIR_NAME)
);
export const InstallsCounter = new Counter("installs", path.dirname(__dirname));

View File

@@ -0,0 +1,54 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { readFile, writeFile } from "fs/promises";
import path from "path";
import { Mutex } from "async-mutex";
type Counts = Record<string, string[]>;
export class Counter {
private path: string;
private readonly mutex: Mutex;
constructor(id: string, baseDirectory: string) {
this.path = path.join(baseDirectory, `${id}.json`);
this.mutex = new Mutex();
}
async increment(key: string, uid: string) {
await this.mutex.runExclusive(async () => {
const counts = await this.counts();
counts[key] = counts[key] || [];
if (counts[key].includes(uid)) return;
counts[key].push(uid);
await this.save(counts);
});
}
private async save(counts: Counts) {
await writeFile(this.path, JSON.stringify(counts));
}
async counts(): Promise<Counts> {
try {
return JSON.parse(await readFile(this.path, "utf-8"));
} catch {
return {};
}
}
}

View File

@@ -0,0 +1,23 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ThemesAPI } from "./api";
export type ThemesRouter = typeof ThemesAPI;
export type { CompiledThemeDefinition, ThemeMetadata } from "./sync";

166
servers/themes/src/orama.ts Normal file
View File

@@ -0,0 +1,166 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Orama, SearchParams, create, search } from "@orama/orama";
import { CompiledThemeDefinition, ThemeMetadata } from "./sync";
import { ThemeQuerySchema } from "./schemas";
export let ThemesDatabase: Orama | null = null;
export async function initializeDatabase(): Promise<Orama> {
ThemesDatabase = await create({
schema: {
id: "string",
name: "string",
authors: { name: "string", email: "string", url: "string" },
colorScheme: "string",
compatibilityVersion: "number",
description: "string",
tags: "string[]"
},
id: "notesnook-themes"
});
return ThemesDatabase;
}
export async function findTheme(
id: string,
compatibilityVersion: number
): Promise<CompiledThemeDefinition | undefined> {
if (!ThemesDatabase) await initializeDatabase();
const results = await search(ThemesDatabase!, {
term: "",
where: {
id,
compatibilityVersion: { eq: compatibilityVersion }
}
});
console.log("EHLO");
return results.hits[0].document as CompiledThemeDefinition;
}
export async function getThemes(query: (typeof ThemeQuerySchema)["_type"]) {
if (!ThemesDatabase) await initializeDatabase();
const from = query.cursor;
const count = query.limit;
const searchParams: SearchParams = {
where: {
compatibilityVersion: {
eq: query.compatibilityVersion
}
}
};
for (const filter of query.filters || []) {
switch (filter.type) {
case "term":
searchParams.term = filter.value;
searchParams.properties = [
"name",
"authors.name",
"description",
"tags",
"id"
];
break;
case "colorScheme":
searchParams.where = {
...searchParams.where,
colorScheme: filter.value
};
break;
}
}
const results = await search(ThemesDatabase!, searchParams);
// results.hits = [
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits,
// ...results.hits
// ];
const themes = results.hits
.map((hit) => {
return {
...(hit.document as CompiledThemeDefinition),
scope: undefined,
codeBlockCSS: undefined
} as ThemeMetadata;
})
.slice(from, from + count);
return {
themes,
nextCursor: (from + count < results.hits.length
? from + count
: undefined) as number | undefined
};
}

View File

@@ -0,0 +1,35 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { THEME_COMPATIBILITY_VERSION } from "@notesnook/theme";
import { z } from "zod";
export const ThemeQuerySchema = z.object({
filters: z
.array(
z.object({
type: z.enum(["term", "colorScheme"]),
value: z.string()
})
)
.optional(),
limit: z.number(),
cursor: z.number().default(0),
compatibilityVersion: z.number().default(THEME_COMPATIBILITY_VERSION)
});

View File

@@ -0,0 +1,39 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { ThemesAPI } from "./api";
import { syncThemes } from "./sync";
import cors from "cors";
const server = createHTTPServer({
middleware: cors(),
router: ThemesAPI
});
const PORT = parseInt(process.env.PORT || "9000");
server.listen(PORT);
console.log(`Server started successfully on: http://localhost:${PORT}/`);
syncThemes();
if (import.meta.hot) {
import.meta.hot.on("vite:beforeFullReload", () => {
server.server.close();
});
}

106
servers/themes/src/sync.ts Normal file
View File

@@ -0,0 +1,106 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { execSync } from "child_process";
import fs from "fs";
import { readdir, readFile } from "fs/promises";
import path from "path";
import {
InstallsCounter,
THEMES_REPO_URL,
THEME_REPO_DIR_PATH
} from "./constants";
import { insertMultiple } from "@orama/orama";
import { initializeDatabase } from "./orama";
import {
ThemeCompatibilityVersion,
ThemeDefinition,
PreviewColors,
getPreviewColors
} from "@notesnook/theme";
export type CompiledThemeDefinition = ThemeDefinition & {
sourceURL?: string;
totalInstalls?: number;
previewColors: PreviewColors;
};
export type ThemeMetadata = Omit<
CompiledThemeDefinition,
"scopes" | "codeBlockCSS"
>;
const THEME_COMPATIBILITY_VERSIONS: ThemeCompatibilityVersion[] = [1];
export async function syncThemes() {
if (!fs.existsSync(THEME_REPO_DIR_PATH)) {
execSync(`git clone ${THEMES_REPO_URL}`, {
stdio: [0, 1, 2],
cwd: path.dirname(THEME_REPO_DIR_PATH)
});
console.log(`Cloned github repo to path ${THEME_REPO_DIR_PATH}`);
} else {
execSync(`git pull`, {
stdio: [0, 1, 2],
cwd: THEME_REPO_DIR_PATH
});
console.log(`Synced github repo ${THEMES_REPO_URL}`);
}
await generateThemesMetadata();
}
async function generateThemesMetadata() {
const themeDefinitions: CompiledThemeDefinition[] = [];
const db = await initializeDatabase();
const THEMES_PATH = path.join(THEME_REPO_DIR_PATH, "themes");
const themes = await readdir(THEMES_PATH);
const counts = await InstallsCounter.counts();
for (const themeId of themes) {
for (const version of THEME_COMPATIBILITY_VERSIONS) {
const themeDirectory = path.join(THEMES_PATH, themeId, `v${version}`);
const themeFilePath = path.join(themeDirectory, "theme.json");
const theme: ThemeDefinition = JSON.parse(
await readFile(themeFilePath, "utf-8")
);
const hasCodeBlockCSS = fs.existsSync(
path.join(themeDirectory, "code-block.css")
);
const codeBlockCSS = await readFile(
path.join(
THEMES_PATH,
hasCodeBlockCSS ? themeId : `default-${theme.colorScheme}`,
`v${version}`,
"code-block.css"
),
"utf-8"
);
themeDefinitions.push({
...theme,
sourceURL: `https://github.com/streetwriters/notesnook-themes/tree/main/themes/${themeId}/v${version}/`,
codeBlockCSS,
totalInstalls: counts[theme.id]?.length || 0,
previewColors: getPreviewColors(theme)
});
}
}
await insertMultiple(db, themeDefinitions);
console.log("Metadata generated and cached.");
}

View File

@@ -0,0 +1,33 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initTRPC } from "@trpc/server";
/**
* Initialization of tRPC backend
* Should be done only once per backend!
*/
const t = initTRPC.create();
/**
* Export reusable router and procedure helpers
* that can be used throughout the router
*/
export const router = t.router;
export const publicProcedure = t.procedure;

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src/", "global.d.ts"]
}

View File

@@ -0,0 +1,29 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
jsxRuntime: "classic"
})
]
});