2021-09-09 22:39:17 +01:00
|
|
|
import styled from "styled-components";
|
|
|
|
|
|
|
|
import { useEffect, useState } from "preact/hooks";
|
|
|
|
|
2021-12-20 12:01:45 +00:00
|
|
|
import { useApplicationState } from "../../../mobx/State";
|
2021-09-09 22:39:17 +01:00
|
|
|
|
2021-12-23 21:43:11 +00:00
|
|
|
import { Theme, generateVariables } from "../../../context/Theme";
|
2021-09-09 22:39:17 +01:00
|
|
|
|
|
|
|
import Tip from "../../../components/ui/Tip";
|
|
|
|
import previewPath from "../assets/preview.svg";
|
|
|
|
|
|
|
|
import { GIT_REVISION } from "../../../revision";
|
2021-09-06 06:02:30 -04:00
|
|
|
|
|
|
|
export const fetchManifest = (): Promise<Manifest> =>
|
2021-09-09 22:39:17 +01:00
|
|
|
fetch(`${import.meta.env.VITE_THEMES_URL}/manifest.json`).then((res) =>
|
|
|
|
res.json(),
|
|
|
|
);
|
2021-09-06 06:02:30 -04:00
|
|
|
|
|
|
|
export const fetchTheme = (slug: string): Promise<Theme> =>
|
2021-09-09 22:39:17 +01:00
|
|
|
fetch(`${import.meta.env.VITE_THEMES_URL}/theme_${slug}.json`).then((res) =>
|
|
|
|
res.json(),
|
|
|
|
);
|
2021-09-06 06:02:30 -04:00
|
|
|
|
2021-09-11 15:40:14 -04:00
|
|
|
export interface ThemeMetadata {
|
2021-09-09 22:39:17 +01:00
|
|
|
name: string;
|
|
|
|
creator: string;
|
|
|
|
commit?: string;
|
|
|
|
description: string;
|
2021-09-06 06:02:30 -04:00
|
|
|
}
|
|
|
|
|
2021-09-11 15:40:14 -04:00
|
|
|
export type Manifest = {
|
2021-09-09 22:39:17 +01:00
|
|
|
generated: string;
|
|
|
|
themes: Record<string, ThemeMetadata>;
|
|
|
|
};
|
2021-09-06 06:02:30 -04:00
|
|
|
|
|
|
|
// TODO: ability to preview / display the settings set like in the appearance pane
|
2021-09-07 02:47:12 -04:00
|
|
|
const ThemeInfo = styled.article`
|
2021-12-20 13:37:21 +00:00
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
gap: 10px;
|
2021-09-09 22:39:17 +01:00
|
|
|
padding: 1rem;
|
|
|
|
border-radius: var(--border-radius);
|
|
|
|
background: var(--secondary-background);
|
|
|
|
|
|
|
|
&[data-loaded] {
|
|
|
|
.preview {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
2021-09-06 06:02:30 -04:00
|
|
|
}
|
|
|
|
|
2021-09-09 22:39:17 +01:00
|
|
|
.preview {
|
|
|
|
grid-area: preview;
|
|
|
|
aspect-ratio: 323 / 202;
|
|
|
|
|
|
|
|
background-color: var(--secondary-background);
|
|
|
|
border-radius: calc(var(--border-radius) / 2);
|
|
|
|
|
|
|
|
// prep style for later
|
|
|
|
outline: 3px solid transparent;
|
|
|
|
|
|
|
|
// hide random svg parts, crop border on firefox
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
// hide until loaded
|
|
|
|
opacity: 0;
|
|
|
|
|
|
|
|
// style button
|
|
|
|
border: 0;
|
|
|
|
margin: 0;
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
transition: 0.25s opacity, 0.25s outline;
|
|
|
|
|
|
|
|
> * {
|
|
|
|
grid-area: 1 / 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
svg {
|
|
|
|
height: 100%;
|
|
|
|
width: 100%;
|
|
|
|
object-fit: contain;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
&:active,
|
|
|
|
&:focus-visible {
|
|
|
|
outline: 3px solid var(--tertiary-background);
|
|
|
|
}
|
2021-09-07 06:31:49 -04:00
|
|
|
}
|
2021-09-06 06:02:30 -04:00
|
|
|
|
2021-09-09 22:39:17 +01:00
|
|
|
.name {
|
2021-12-20 13:37:21 +00:00
|
|
|
margin-top: 5px !important;
|
2021-09-09 22:39:17 +01:00
|
|
|
grid-area: name;
|
|
|
|
margin: 0;
|
|
|
|
}
|
2021-09-06 06:02:30 -04:00
|
|
|
|
2021-09-09 22:39:17 +01:00
|
|
|
.creator {
|
|
|
|
grid-area: creator;
|
|
|
|
justify-self: end;
|
|
|
|
font-size: 0.75rem;
|
|
|
|
}
|
2021-09-06 06:02:30 -04:00
|
|
|
|
2021-09-09 22:39:17 +01:00
|
|
|
.description {
|
2021-12-20 13:37:21 +00:00
|
|
|
margin-bottom: 5px;
|
2021-09-09 22:39:17 +01:00
|
|
|
grid-area: desc;
|
|
|
|
}
|
2021-12-20 13:37:21 +00:00
|
|
|
|
|
|
|
.previewBox {
|
|
|
|
position: relative;
|
|
|
|
height: 100%;
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
.hover {
|
|
|
|
opacity: 0;
|
|
|
|
font-family: var(--font), sans-serif;
|
|
|
|
font-variant-ligatures: var(--ligatures);
|
|
|
|
font-weight: 600;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
color: white;
|
|
|
|
height: 100%;
|
|
|
|
width: 100%;
|
|
|
|
z-index: 10;
|
|
|
|
position: absolute;
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
cursor: pointer;
|
|
|
|
transition: opacity 0.2s ease-in-out;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
> svg {
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
}
|
2021-09-09 22:39:17 +01:00
|
|
|
`;
|
2021-09-06 06:02:30 -04:00
|
|
|
|
|
|
|
const ThemeList = styled.div`
|
2021-09-09 22:39:17 +01:00
|
|
|
display: grid;
|
2021-12-20 13:37:21 +00:00
|
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
2021-09-09 22:39:17 +01:00
|
|
|
gap: 1rem;
|
|
|
|
`;
|
2021-09-06 06:02:30 -04:00
|
|
|
|
2021-12-20 13:37:21 +00:00
|
|
|
const Banner = styled.div`
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const Category = styled.div`
|
|
|
|
display: flex;
|
|
|
|
gap: 8px;
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
.title {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
gap: 8px;
|
|
|
|
flex-grow: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.view {
|
|
|
|
font-size: 12px;
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const ActiveTheme = styled.div`
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
background: var(--secondary-background);
|
|
|
|
padding: 0;
|
|
|
|
border-radius: var(--border-radius);
|
|
|
|
gap: 8px;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
.active-indicator {
|
|
|
|
display: flex;
|
|
|
|
gap: 6px;
|
|
|
|
align-items: center;
|
|
|
|
background: var(--accent);
|
|
|
|
width: 100%;
|
|
|
|
padding: 5px 10px;
|
|
|
|
font-size: 13px;
|
|
|
|
font-weight: 400;
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
.title {
|
|
|
|
font-size: 1.2rem;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
|
|
|
|
.author {
|
|
|
|
font-size: 12px;
|
|
|
|
margin-bottom: 5px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.theme {
|
|
|
|
width: 124px;
|
|
|
|
height: 80px;
|
|
|
|
background: var(--tertiary-background);
|
|
|
|
border-radius: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.container {
|
|
|
|
display: flex;
|
|
|
|
gap: 16px;
|
|
|
|
padding: 10px 16px 16px;
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
2021-09-06 06:02:30 -04:00
|
|
|
const ThemedSVG = styled.svg<{ theme: Theme }>`
|
2021-09-09 22:39:17 +01:00
|
|
|
${(props) => props.theme && generateVariables(props.theme)}
|
|
|
|
`;
|
2021-09-06 06:02:30 -04:00
|
|
|
|
|
|
|
type ThemePreviewProps = Omit<JSX.HTMLAttributes<SVGSVGElement>, "as"> & {
|
2021-09-09 22:39:17 +01:00
|
|
|
slug?: string;
|
|
|
|
theme?: Theme;
|
|
|
|
onThemeLoaded?: (theme: Theme) => void;
|
2021-09-06 06:02:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => {
|
2021-09-09 22:39:17 +01:00
|
|
|
return (
|
|
|
|
<ThemedSVG
|
|
|
|
{...props}
|
|
|
|
theme={theme}
|
|
|
|
width="323"
|
|
|
|
height="202"
|
|
|
|
aria-hidden="true"
|
|
|
|
data-loaded={!!theme}>
|
|
|
|
<use href={`${previewPath}#preview`} width="100%" height="100%" />
|
|
|
|
</ThemedSVG>
|
|
|
|
);
|
|
|
|
};
|
2021-09-06 06:02:30 -04:00
|
|
|
|
2021-09-07 02:47:12 -04:00
|
|
|
const ThemeShopRoot = styled.div`
|
2021-09-09 22:39:17 +01:00
|
|
|
display: grid;
|
|
|
|
gap: 1rem;
|
2021-12-20 13:37:21 +00:00
|
|
|
|
|
|
|
h5 {
|
|
|
|
margin-bottom: 0;
|
|
|
|
}
|
2021-09-09 22:39:17 +01:00
|
|
|
`;
|
2021-09-07 02:47:12 -04:00
|
|
|
|
2021-09-06 06:02:30 -04:00
|
|
|
export function ThemeShop() {
|
2021-09-09 22:39:17 +01:00
|
|
|
// setThemeList is for adding more / lazy loading in the future
|
|
|
|
const [themeList, setThemeList] = useState<
|
|
|
|
[string, ThemeMetadata][] | null
|
|
|
|
>(null);
|
|
|
|
const [themeData, setThemeData] = useState<Record<string, Theme>>({});
|
|
|
|
|
2021-12-20 12:01:45 +00:00
|
|
|
const themes = useApplicationState().settings.theme;
|
|
|
|
|
2021-09-09 22:39:17 +01:00
|
|
|
async function fetchThemeList() {
|
|
|
|
const manifest = await fetchManifest();
|
|
|
|
setThemeList(
|
|
|
|
Object.entries(manifest.themes).filter((x) =>
|
|
|
|
x[1].commit ? x[1].commit === GIT_REVISION : true,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getTheme(slug: string) {
|
|
|
|
const theme = await fetchTheme(slug);
|
|
|
|
setThemeData((data) => ({ ...data, [slug]: theme }));
|
|
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
fetchThemeList();
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
themeList?.forEach(([slug]) => {
|
|
|
|
getTheme(slug);
|
|
|
|
});
|
|
|
|
}, [themeList]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ThemeShopRoot>
|
2021-12-20 13:37:21 +00:00
|
|
|
<h5>
|
|
|
|
Browse hundreds of themes, created and curated by the community.
|
|
|
|
</h5>
|
|
|
|
{/*<LoadFail>
|
|
|
|
<h5>
|
|
|
|
Oops! Couldn't load the theme shop. Make sure you're
|
|
|
|
connected to the internet and try again.
|
|
|
|
</h5>
|
|
|
|
</LoadFail>*/}
|
2021-09-09 22:39:17 +01:00
|
|
|
<Tip warning hideSeparator>
|
2021-12-20 13:37:21 +00:00
|
|
|
The Theme Shop is currently under construction.
|
2021-09-09 22:39:17 +01:00
|
|
|
</Tip>
|
2021-12-23 18:21:42 +01:00
|
|
|
<hr />
|
2021-12-20 13:37:21 +00:00
|
|
|
{/* FIXME INTEGRATE WITH MOBX */}
|
|
|
|
{/*<ActiveTheme>
|
|
|
|
<div class="active-indicator">
|
|
|
|
<Check size="16" />
|
|
|
|
Currently active
|
|
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<div class="theme">theme svg goes here</div>
|
|
|
|
<div class="info">
|
|
|
|
<div class="title">Theme Title</div>
|
|
|
|
<div class="author">by Author</div>
|
|
|
|
<h5>This is a theme description.</h5>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</ActiveTheme>
|
|
|
|
<InputBox placeholder="Search themes..." contrast />
|
|
|
|
<Category>
|
|
|
|
<div class="title">
|
|
|
|
<Bookmark size={16} />
|
|
|
|
Saved
|
|
|
|
</div>
|
|
|
|
<a class="view">Manage installed</a>
|
|
|
|
</Category>
|
|
|
|
|
|
|
|
<Category>
|
|
|
|
<div class="title">
|
|
|
|
<Star size={16} />
|
|
|
|
New this week
|
|
|
|
</div>
|
|
|
|
<a class="view">View all</a>
|
|
|
|
</Category>
|
|
|
|
|
|
|
|
<Category>
|
|
|
|
<div class="title">
|
|
|
|
<Brush size={16} />
|
|
|
|
Default themes
|
|
|
|
</div>
|
|
|
|
<a class="view">View all</a>
|
|
|
|
</Category>
|
|
|
|
|
|
|
|
<Category>
|
|
|
|
<div class="title">
|
|
|
|
<BarChartAlt2 size={16} />
|
|
|
|
Highest rated
|
|
|
|
</div>
|
|
|
|
<a class="view">View all</a>
|
|
|
|
</Category>*/}
|
|
|
|
|
2021-09-09 22:39:17 +01:00
|
|
|
<ThemeList>
|
|
|
|
{themeList?.map(([slug, theme]) => (
|
|
|
|
<ThemeInfo
|
|
|
|
key={slug}
|
|
|
|
data-loaded={Reflect.has(themeData, slug)}>
|
|
|
|
<button
|
|
|
|
class="preview"
|
2021-12-24 11:45:49 +00:00
|
|
|
onClick={() =>
|
|
|
|
themes.hydrate(themeData[slug], true)
|
|
|
|
}>
|
2021-12-20 13:37:21 +00:00
|
|
|
<div class="previewBox">
|
|
|
|
<div class="hover">Use theme</div>
|
|
|
|
<ThemePreview
|
|
|
|
slug={slug}
|
|
|
|
theme={themeData[slug]}
|
|
|
|
/>
|
|
|
|
</div>
|
2021-09-09 22:39:17 +01:00
|
|
|
</button>
|
2021-12-20 13:37:21 +00:00
|
|
|
<h1 class="name">{theme.name}</h1>
|
|
|
|
{/* Maybe id's of the users should be included as well / instead? */}
|
|
|
|
<div class="creator">by {theme.creator}</div>
|
|
|
|
<h5 class="description">{theme.description}</h5>
|
2021-09-09 22:39:17 +01:00
|
|
|
</ThemeInfo>
|
|
|
|
))}
|
|
|
|
</ThemeList>
|
|
|
|
</ThemeShopRoot>
|
|
|
|
);
|
|
|
|
}
|