Skip to content

Commit

Permalink
poc
Browse files Browse the repository at this point in the history
  • Loading branch information
lelemm committed Dec 12, 2024
1 parent 62d8358 commit 7c44a7d
Show file tree
Hide file tree
Showing 19 changed files with 8,176 additions and 3,950 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"typescript-strict-plugin": "^2.4.4"
},
"resolutions": {
"rollup": "4.9.4"
"rollup": "4.9.4",
"react": "18.2.0"
},
"engines": {
"node": ">=18.0.0"
Expand Down
1 change: 1 addition & 0 deletions packages/desktop-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"inter-ui": "^3.19.3",
"jest": "^27.5.1",
"jest-watch-typeahead": "^2.2.2",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"mdast-util-newline-to-break": "^2.0.0",
"memoize-one": "^6.0.0",
Expand Down
112 changes: 112 additions & 0 deletions packages/desktop-client/public/index.es.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from "react";
import React__default from "react";
const SvgPenTool = (props) => /* @__PURE__ */ React.createElement(
"svg",
{
...props,
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 20 20",
style: {
color: "inherit",
...props.style
}
},
/* @__PURE__ */ React.createElement(
"path",
{
d: "M11 9.27V0l6 11-4 6H7l-4-6L9 0v9.27a2 2 0 1 0 2 0zM6 18h8v2H6v-2z",
fill: "currentColor"
}
)
);
const ThemeIcon = ({ themeName, darkMode, style }) => {
switch (themeName) {
case "dracula":
return /* @__PURE__ */ React__default.createElement(SvgPenTool, { style });
}
return /* @__PURE__ */ React__default.createElement("div", { style });
};
const draculaTheme = {
pageBackground: "#282a36",
// Dark background
pageText: "#f8f8f2",
// Light text
pageTextSubdued: "#6272a4",
// Comments
cardBackground: "#44475a",
// Current line background
cardBorder: "#bd93f9",
// Purple border
cardShadow: "rgba(0, 0, 0, 0.5)",
// Slight shadow
tableBackground: "#282a36",
tableRowBackgroundHover: "#44475a",
tableText: "#f8f8f2",
tableTextSubdued: "#6272a4",
tableBorder: "#44475a",
sidebarBackground: "#282a36",
sidebarItemText: "#f8f8f2",
sidebarItemTextSelected: "#ff79c6",
// Pink for active selection
sidebarItemBackgroundHover: "#44475a",
menuBackground: "#44475a",
menuItemText: "#f8f8f2",
menuItemTextHover: "#ff79c6",
menuItemTextSelected: "#50fa7b",
// Green for selected items
menuBorder: "#6272a4",
buttonPrimaryText: "#282a36",
buttonPrimaryBackground: "#50fa7b",
// Green
buttonPrimaryBorder: "#50fa7b",
buttonPrimaryDisabledText: "#6272a4",
buttonPrimaryDisabledBackground: "#44475a",
buttonNormalText: "#f8f8f2",
buttonNormalBackground: "#44475a",
buttonNormalBorder: "#6272a4",
buttonBareText: "#f8f8f2",
buttonBareBackgroundHover: "rgba(255, 121, 198, 0.2)",
// Pink hover effect
noticeBackground: "#50fa7b",
noticeText: "#282a36",
noticeBorder: "#6272a4",
warningBackground: "#ffb86c",
warningText: "#282a36",
warningBorder: "#bd93f9",
errorBackground: "#ff5555",
errorText: "#282a36",
errorBorder: "#ff79c6",
formInputBackground: "#44475a",
formInputBorder: "#6272a4",
formInputText: "#f8f8f2",
formInputTextPlaceholder: "#6272a4",
calendarBackground: "#282a36",
calendarItemBackground: "#44475a",
calendarItemText: "#f8f8f2",
calendarSelectedBackground: "#bd93f9",
pillBackground: "#44475a",
pillText: "#f8f8f2",
pillBackgroundSelected: "#bd93f9",
tooltipText: "#f8f8f2",
tooltipBackground: "#6272a4",
tooltipBorder: "#bd93f9",
floatingActionBarBackground: "#44475a",
floatingActionBarBorder: "#bd93f9",
floatingActionBarText: "#f8f8f2",
markdownNormal: "#ff79c6",
markdownDark: "#bd93f9",
markdownLight: "#50fa7b"
};
const plugin = {
name: "Example",
version: "0.0.1",
availableThemes: (darkMode) => darkMode ? ["dracula"] : ["allwhite"],
getThemeIcon: (themeName, darkMode, properties) => /* @__PURE__ */ React__default.createElement(ThemeIcon, { themeName, darkMode, style: properties }),
getThemeSchema: themeSchema
};
function themeSchema(themeName, darkMode) {
return draculaTheme;
}
export {
plugin as default
};
64 changes: 59 additions & 5 deletions packages/desktop-client/src/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, { useRef, useState, type CSSProperties } from 'react';
import React, { ReactNode, useEffect, useRef, useState, type CSSProperties } from 'react';

import { t } from 'i18next';

import type { Theme } from 'loot-core/src/types/prefs';

import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2';
import { themeOptions, useTheme } from '../style';
import { themeOptions, themes, useTheme } from '../style';

import { Button } from './common/Button2';
import { Menu } from './common/Menu';
import { Popover } from './common/Popover';
import { useResponsive } from './responsive/ResponsiveProvider';
import { loadedPlugins } from '../pluginLoader';

type ThemeSelectorProps = {
style?: CSSProperties;
Expand All @@ -20,16 +21,69 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
const [theme, switchTheme] = useTheme();
const [menuOpen, setMenuOpen] = useState(false);
const triggerRef = useRef(null);
const [themesExtended, setThemesExtended] = useState(themes);
const [themeOptionsExtended, setThemeOptionsExtended] = useState(themeOptions);

const { isNarrowWidth } = useResponsive();

const themeIcons = {
const baseIcons = {
light: SvgSun,
dark: SvgMoonStars,
auto: SvgSystem,
midnight: SvgMoonStars,
development: SvgMoonStars,
} as const;
};
const [themeIcons, setThemeIcons] = useState(baseIcons);

useEffect(() => {
debugger;

const pluginIconsLight = loadedPlugins.reduce((acc, plugin) => {
if (plugin.availableThemes?.length) {
plugin.availableThemes(false).forEach(theme => {
acc[theme] = (props: React.SVGProps<SVGSVGElement>) => plugin.getThemeIcon(theme, false, props.style);
});
}
return acc;
}, {});

const pluginIconsDark = loadedPlugins.reduce((acc, plugin) => {
if (plugin.availableThemes?.length) {
plugin.availableThemes(true).forEach(theme => {
acc[theme] = (props: React.SVGProps<SVGSVGElement>) => plugin.getThemeIcon(theme, true, props.style);
});
}
return acc;
}, {});

const themesLight = loadedPlugins.reduce((acc, plugin) => {
if (plugin.availableThemes?.length) {
plugin.availableThemes(false).forEach(theme => {
acc[theme] = { name: theme, colors: plugin.getThemeSchema(theme, false)};
});
}
return acc;
}, {});

const themesDark = loadedPlugins.reduce((acc, plugin) => {
if (plugin.availableThemes?.length) {
plugin.availableThemes(true).forEach(theme => {
acc[theme] = { name: theme, colors: plugin.getThemeSchema(theme, true)};
});
}
return acc;
}, {});

setThemeIcons({ ...baseIcons, ...pluginIconsLight, ...pluginIconsDark });

setThemesExtended({...themes, ...themesLight, ...themesDark})
}, [loadedPlugins]);

useEffect(() => {
setThemeOptionsExtended(Object.entries(themesExtended).map(
([key, { name }]) => [key, name] as [Theme, string],
));
}, [themesExtended]);

function onMenuSelect(newTheme: Theme) {
setMenuOpen(false);
Expand Down Expand Up @@ -62,7 +116,7 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
>
<Menu
onMenuSelect={onMenuSelect}
items={themeOptions.map(([name, text]) => ({ name, text }))}
items={themeOptionsExtended.map(([name, text]) => ({ name, text }))}
/>
</Popover>
</>
Expand Down
134 changes: 134 additions & 0 deletions packages/desktop-client/src/pluginLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React from 'react';
import { ActualPlugin } from '../../plugins-shared/src';
import * as jszip from 'jszip';

export var loadedPlugins: ActualPlugin[] = null;
loadPlugins().then(plugins => {
loadedPlugins = plugins;
});

export async function loadPlugins(): Promise<ActualPlugin[]> {
return [
await loadPluginFromRepo('https://github.com/actual-plugins/example'),
];
}

async function loadPluginScript(scriptBlob: Blob): Promise<ActualPlugin> {
window.React = React;

var script = await scriptBlob.text();
let adjustedContent = script.replace('import * as React from "react";', 'const React = window.React;');
adjustedContent = adjustedContent.replace('import React__default from "react";', 'const React__default = window.React;');
const scriptBlobAdjusted = new Blob([adjustedContent], { type: "application/javascript" });
const scriptURL = URL.createObjectURL(scriptBlobAdjusted);
const pluginModule = await import(/* @vite-ignore */ scriptURL);

if (pluginModule?.default) {
const plugin: ActualPlugin = pluginModule.default;
console.log(
`Plugin "${plugin.name}" v${plugin.version} loaded successfully.`,
);
return plugin;
}
throw new Error('Plugin script does not export a default object.');
}

async function fetchLatestRelease(
owner: string,
repo: string,
): Promise<{ version: string; zipUrl: string }> {
debugger;
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
const response = await fetch(apiUrl);
if (!response.ok)
throw new Error(`Failed to fetch release metadata for ${repo}`);

const releaseData = await response.json();
const version = releaseData.tag_name;
const zipUrl = releaseData.assets.filter(f => f.name.includes('.zip'))[0]
?.browser_download_url;

return { version, zipUrl };
}

function parseGitHubRepoUrl(
url: string,
): { owner: string; repo: string } | null {
try {
const parsedUrl = new URL(url);

if (!parsedUrl.hostname.includes('github.com')) {
throw new Error('Not a valid GitHub URL');
}

const pathParts = parsedUrl.pathname.split('/').filter(Boolean); // Remove empty parts

if (pathParts.length >= 2) {
const owner = pathParts[0];
const repo = pathParts[1];
return { owner, repo };
}

throw new Error('URL does not contain owner and repository name');
} catch (error) {
console.error(`Error parsing GitHub URL: ${url}`, error.message);
return null;
}
}

async function loadPluginFromRepo(repo: string): Promise<ActualPlugin | null> {
try {
const parsedRepo = parseGitHubRepoUrl(repo);
if (parsedRepo == null) {
throw new Error(`Invalid repo ${repo}`);
}

console.log(`Checking for updates for plugin ${repo}...`);

const { version: latestVersion, zipUrl } = await fetchLatestRelease(
parsedRepo.owner,
parsedRepo.repo,
);

// // Check if the plugin is already cached
// const cachedPlugin = await getPlugin(repo);

// if (cachedPlugin && cachedPlugin.version === latestVersion) {
// console.log(`Using cached plugin "${repo}" v${latestVersion}`);
// return await loadPluginScript(cachedPlugin.script);
// }

console.log(`Downloading plugin "${repo}" v${latestVersion}...`);

// Download the ZIP file
//const response = await fetch(zipUrl);
//const response = await fetch('http://localhost:3001/example.zip');
const response = await fetch('http://localhost:3001/index.es.js');

if (!response.ok)
throw new Error(`Failed to download plugin ZIP for ${repo}`);

/*const zipBlob = await response.blob();
// Extract index.js
const zip = await jszip.loadAsync(zipBlob);
const indexJsPath = Object.keys(zip.files).find(file =>
file.endsWith('index.cjs.js'),
);
if (!indexJsPath)
throw new Error(`No index.js file found in the plugin ZIP.`);
const indexJsBlob = await zip.files[indexJsPath].async('blob');*/
const indexJsBlob = await response.blob();

// Save to cache
//await savePlugin(repo, latestVersion, indexJsBlob);
console.log(`Plugin "${repo}" cached successfully.`);

// Load the plugin dynamically
return await loadPluginScript(indexJsBlob);
} catch (error) {
console.error(`Error loading plugin "${repo}":`, error);
return null;
}
}
2 changes: 1 addition & 1 deletion packages/desktop-client/src/style/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as developmentTheme from './themes/development';
import * as lightTheme from './themes/light';
import * as midnightTheme from './themes/midnight';

const themes = {
export const themes = {
light: { name: 'Light', colors: lightTheme },
dark: { name: 'Dark', colors: darkTheme },
midnight: { name: 'Midnight', colors: midnightTheme },
Expand Down
4 changes: 2 additions & 2 deletions packages/loot-core/src/types/prefs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export type LocalPrefs = Partial<{
'mobile.showSpentColumn': boolean;
}>;

export type Theme = 'light' | 'dark' | 'auto' | 'midnight' | 'development';
export type DarkTheme = 'dark' | 'midnight';
export type Theme = 'light' | 'dark' | 'auto' | 'midnight' | 'development' | (string & {});
export type DarkTheme = 'dark' | 'midnight' | (string & {});
export type GlobalPrefs = Partial<{
floatingSidebar: boolean;
maxMonths: number;
Expand Down
4 changes: 4 additions & 0 deletions packages/plugins-shared/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated
Loading

0 comments on commit 7c44a7d

Please sign in to comment.