feat: Added plugins section and made it more mobile friendly
All checks were successful
Build Gameflow Site / build (push) Successful in 1m19s

This commit is contained in:
Simeon Radivoev 2026-05-10 17:51:46 +03:00
parent 13f1d97394
commit 87f8f485aa
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
26 changed files with 331 additions and 63 deletions

View file

@ -0,0 +1,3 @@
{
"alt": "Gameflow 3D Screenshow hero image"
}

View file

@ -0,0 +1,3 @@
{
"alt": "Game details screen of the app. Showing the rom Peter Jackson's King Kong Game in dark mode."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing the responsive layout and the experimental portrait layout in specific."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Game details screen of the app. Showing the rom Peter Jackson's King Kong Game in light mode."
}

View file

@ -0,0 +1,3 @@
{
"atl": "The free in app store home page. Showing recommended free games and emulators."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing the gameflow platform list. Where users can find games based on platforms. Like Xbox or Playstation"
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing the plugins page for the gameflow app. It shows internal plugins built into the app. Like ES-DE autoconfig or different emulator implementations. All this in dark mode."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing all emulator for download that will be fully supported by gameflow. Where you can easily install emulators and download firmware if you have romm setup. All this in light mode."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing the homescreen of the app. The default filter is most activity. Be it last played or freshly installed. In light mode."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing the plugins page for the gameflow app. It shows internal plugins built into the app. Like ES-DE autoconfig or different emulator implementations. All this in light mode."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing the homescreen of the app. The default filter is most activity. Be it last played or freshly installed. In dark mode."
}

View file

@ -0,0 +1,3 @@
{
"alt": "Showing all emulator for download that will be fully supported by gameflow. Where you can easily install emulators and download firmware if you have romm setup. All this in dark mode."
}

35
src/components/Dock.astro Normal file
View file

@ -0,0 +1,35 @@
---
import { HandCoins, House, Puzzle } from "@lucide/astro";
import { Github } from "simple-icons-astro";
import pkg from "../../package.json";
---
<div class="md:hidden dock dock-sm" style="view-transition-name: footer;">
<a
data-active={Astro.url.pathname === `${import.meta.env.BASE_URL}/`}
class="data-[active=true]:dock-active"
href={import.meta.env.BASE_URL}
>
<House />
<span class="dock-label">Home</span>
</a>
<a
data-active={Astro.url.pathname.startsWith(
`${import.meta.env.BASE_URL}/plugins`,
)}
class="data-[active=true]:dock-active"
href={`${import.meta.env.BASE_URL}plugins`}
>
<Puzzle />
<span class="dock-label">Plugins</span>
</a>
<a href={pkg.socials.github}>
<Github />
<span class="dock-label">Github</span>
</a>
<a href={pkg.sponsor.url}>
<HandCoins />
<span class="dock-label">Sponsor</span>
</a>
</div>

View file

@ -2,7 +2,7 @@
const { title, description, icon } = Astro.props;
---
<div class="flex gap-4 justify-center m-4 p-4 ring-2 ring-accent/5 rounded-2xl">
<div class="flex gap-4 m-4 p-4 ring-2 ring-accent/5 rounded-2xl">
<slot name="icon" />
<div class="flex flex-col">
<h1>{title}</h1>

View file

@ -9,6 +9,7 @@ import {
Store,
RefreshCcw,
Joystick,
Puzzle,
} from "@lucide/astro";
import Feature from "./Feature.astro";
---
@ -76,4 +77,10 @@ import Feature from "./Feature.astro";
>
<SunMoon size={64} slot="icon" />
</Feature>
<Feature
title="Plugins System"
description="Extent functionality with plugins. Hosted on the NPM registry."
>
<Puzzle size={64} slot="icon" />
</Feature>
</div>

View file

@ -1,5 +1,5 @@
---
import { Github, Youtube } from "simple-icons-astro";
import { Discord, Github, Youtube } from "simple-icons-astro";
import { HandCoins } from "@lucide/astro";
import pkg from "../../package.json";
---
@ -11,11 +11,17 @@ import pkg from "../../package.json";
<a class="link link-hover" href={pkg.downloads}>Downloads</a>
<a class="link link-hover" href={pkg.socials.mirror}>Mirror</a>
<a class="link link-hover" href={pkg.socials.license}>License</a>
<a class="link link-hover" href={pkg.socials.packages}>Packages</a>
</nav>
<nav>
<h6 class="footer-title">Navigation</h6>
<a class="link link-hover" href={`${import.meta.env.BASE_URL}plugins`}
>Plugins</a
>
</nav>
<nav>
<h6 class="footer-title">Others</h6>
<a class="link link-hover" href={pkg.socials.aboutus}>About us</a>
<a class="link link-hover" href={pkg.socials.packages}>Packages</a>
<a class="link link-hover" href={pkg.socials.store}>Store</a>
<a class="link link-hover" href={pkg.socials.discord}>Discord</a>
<a class="link link-hover" href={pkg.sponsor.url}>Sponsor</a>
@ -32,6 +38,9 @@ import pkg from "../../package.json";
<a title="Sponsor" href={pkg.sponsor.url}>
<HandCoins />
</a>
<a title="Discord" href={pkg.socials.discord}>
<Discord />
</a>
</div>
</nav>
</footer>

View file

@ -1,33 +1,52 @@
---
import { Github, Discord } from "simple-icons-astro";
import { HandCoins } from "@lucide/astro";
import { HandCoins, Puzzle } from "@lucide/astro";
import Icon from "../assets/icon.svg";
import pkg from "../../package.json";
import { releaseData } from "../scripts/getters";
import { plugins, releaseData } from "../scripts/getters";
import Image from "astro/components/Image.astro";
---
<div class="sticky flex top-0 navbar z-100 bg-base-200 justify-between">
<div class="flex gap-2 items-center">
<img class="size-12" src={Icon.src} />
<div
class="sticky flex flex-wrap gap-2 top-0 navbar z-100 bg-base-200 justify-between"
style="view-transition-name: header;"
>
<a class="flex gap-2 items-center" href={import.meta.env.BASE_URL}>
<Image src={Icon} class="size-12" alt="Gameflow Hero Screenshot" />
<div class="text-primary font-bold text-lg uppercase">
{pkg.displayName}
</div>
<div>Deck</div>
<div class="bg-base-300 p-1 px-3 rounded-full">{releaseData.tag_name}</div>
</div>
<div class="flex gap-2">
</a>
<div class="hidden md:flex gap-2">
<a
title="View a list of plugins you can download for gameflow"
data-active={Astro.url.pathname.startsWith(
`${import.meta.env.BASE_URL}/plugins`,
)}
class="flex group gap-2 bg-base-300 data-[active=true]:cursor-default rounded-full p-1 px-3 text-xl hover:bg-accent hover:text-accent-content data-[active=true]:bg-accent data-[active=true]:text-accent-content items-center"
href={`${import.meta.env.BASE_URL}plugins`}
><Puzzle />Plugins<span
class="bg-base-100 px-2 text-sm font-semibold rounded-full group-hover:bg-accent-content group-hover:text-accent in-data-[active=true]:bg-accent-content in-data-[active=true]:text-accent-content"
>{plugins.total}</span
>
</a>
<div class="divider divider-horizontal"></div>
<a
title="Support Gameflow development"
class="flex gap-2 bg-base-300 cursor-pointer rounded-full p-1 px-3 text-xl hover:bg-accent hover:text-accent-content"
href={pkg.sponsor.url}
><HandCoins />
</a>
<a
class="hidden md:flex gap-2 bg-base-300 cursor-pointer rounded-full p-1 px-3 text-xl hover:bg-accent hover:text-accent-content"
class="flex gap-2 bg-base-300 cursor-pointer rounded-full p-1 px-3 text-xl hover:bg-accent hover:text-accent-content"
href={pkg.socials.discord}
title="Join our Community on Discord"
><Discord />
</a>
<a
title="Sponsor"
title="View source code on GitHub"
class="hidden md:flex gap-2 bg-base-300 cursor-pointer rounded-full p-1 px-3 text-xl hover:bg-accent hover:text-accent-content"
href={pkg.socials.github}
><Github />GitHub

View file

@ -1,9 +1,10 @@
---
import screenshot from "../assets/screenshots/3dscreenshot.webp";
import { Dot, Download } from "@lucide/astro";
import { Dot, Download, Milestone, Scale } from "@lucide/astro";
import { Github } from "simple-icons-astro";
import pkg from "../../package.json";
import { Image } from "astro:assets";
import { releaseData } from "../scripts/getters";
---
<div class="hero bg-base-100 py-16 md:p-0 md:min-h-[calc(100vh-19rem)]">
@ -17,9 +18,13 @@ import { Image } from "astro:assets";
<small
class="flex text-base-content/40 gap-2 justify-center md:justify-start items-center mb-2"
>
<Github size={16} /> Open Source <Dot />
<Github size={16} /> Open Source
<Dot />
<Scale />
{pkg.license}
<div class="badge badge-ghost">beta</div>
<Dot />
<Milestone />
{releaseData.tag_name}
</small>
<h1
class="flex flex-wrap gap-4 text-5xl font-bold text-primary justify-center md:justify-start"
@ -29,12 +34,14 @@ import { Image } from "astro:assets";
</h1>
<p class="py-6">
A Cross-Platform open source Retro gaming frontend designed for handheld
and controllers. Focused on building a simple user experience and
intuitive UI as a curated community driven experience.
and controllers. Focused on building a simple user experience and fluent
and intuitive UI as a curated community driven experience. Download and
play what you need and let the app do the rest.
</p>
<div class="flex flex-wrap gap-2 justify-center md:justify-start">
<a
class="bg-primary rounded-full text-primary-content p-2 px-4 hover:bg-secondary hover:text-secondary-content cursor-pointer flex gap-2"
title="Download Gameflow from GitHub releases"
href={pkg.downloads}
target="_blank"
>
@ -43,6 +50,7 @@ import { Image } from "astro:assets";
</a>
<a
class="bg-primary rounded-full text-primary-content p-2 px-4 hover:bg-accent hover:text-accent-content cursor-pointer flex gap-2"
title="View Source code on GitHub. As well as issues"
href={pkg.socials.github}
target="_blank"
>

View file

@ -4,6 +4,15 @@ import { Image } from "astro:assets";
const images = import.meta.glob("../assets/screenshots/*.webp", {
eager: true,
});
const metas = import.meta.glob("../assets/screenshots/*.json", {
eager: true,
});
const items = Object.entries(images).map(([imgPath, img]) => {
const metaPath = imgPath.replace(/\.(jpg|jpeg|png|webp)$/, ".json");
const meta = metas[metaPath]?.default ?? {};
return { image: img.default, meta, path: imgPath };
});
---
<div class="divider sm:my-8 md:my-16"><ImageIcon size={48} />Screenshots</div>
@ -11,15 +20,19 @@ const images = import.meta.glob("../assets/screenshots/*.webp", {
class="columns-1 sm:columns-2 md:columns-3 lg:columns-4 gap-4 sm:p-8 md:p-16"
>
{
Object.values(images).map((img: any, i) => (
<a href={img.default.src} target="_blank">
<Image
class="mb-4 w-full rounded-xl"
alt={`Screenshot ${i}`}
src={img.default}
loading="lazy"
/>
</a>
))
items.map(({ image, meta }, i) => {
const element = (
<a href={image.src} target="_blank">
<Image
class="mb-4 w-full rounded-xl"
alt={meta.alt ?? `Screenshot ${i}`}
src={image}
loading="lazy"
/>
</a>
);
return element;
})
}
</div>

View file

@ -5,6 +5,7 @@ import {
Star,
GitCommitVertical,
UserPen,
Puzzle,
} from "@lucide/astro";
import {
repoData,
@ -12,6 +13,7 @@ import {
appContributorsData,
storeContributorsData,
totalDownloads,
plugins,
} from "../scripts/getters";
---
@ -21,34 +23,28 @@ import {
<div class="flex gap-4">
<Star class="text-primary" size={48} />
<div class="flex flex-col">
<span id="stars" class="text-2xl font-semibold"
>{repoData.stargazers_count}+</span
>
<span class="text-2xl font-semibold">{repoData.stargazers_count}+</span>
<div class="text-base-content/60">GitHub Stars</div>
</div>
</div>
<div class="flex gap-4">
<Download class="text-primary" size={48} />
<div class="flex flex-col">
<span id="downloads" class="text-2xl font-semibold"
>{totalDownloads}+</span
>
<span class="text-2xl font-semibold">{totalDownloads}+</span>
<div class="text-base-content/60">Downloads</div>
</div>
</div>
<div class="flex gap-4">
<Joystick class="text-primary" size={48} />
<div class="flex flex-col">
<span id="downloads" class="text-2xl font-semibold"
>{emulators.length}</span
>
<span class="text-2xl font-semibold">{emulators.length}</span>
<div class="text-base-content/60">Built In Emulators</div>
</div>
</div>
<div class="flex gap-4">
<GitCommitVertical class="text-primary" size={48} />
<div class="flex flex-col">
<span id="downloads" class="text-2xl font-semibold"
<span class="text-2xl font-semibold"
>{
appContributorsData.reduce(
(sum: number, user: any) => sum + user.contributions,
@ -62,7 +58,7 @@ import {
<div class="flex gap-4">
<UserPen class="text-primary" size={48} />
<div class="flex flex-col">
<span id="downloads" class="text-2xl font-semibold"
<span class="text-2xl font-semibold"
>{
new Set(
appContributorsData
@ -74,4 +70,11 @@ import {
<div class="text-base-content/60">Contributors</div>
</div>
</div>
<div class="flex gap-4">
<Puzzle class="text-primary" size={48} />
<div class="flex flex-col">
<span class="text-2xl font-semibold">{plugins.total}</span>
<div class="text-base-content/60">Plugins</div>
</div>
</div>
</div>

View file

@ -11,7 +11,6 @@ import Features from "./Features.astro";
import Community from "./Community.astro";
---
<Header />
<Hero />
<Platforms />
<Stats />

View file

@ -3,11 +3,13 @@ import "../assets/style.css";
import favicon from "../assets/favicon.ico";
import pkg from "../../package.json";
import preview from "../assets/screenshots/3dscreenshot.webp";
import { ClientRouter } from "astro:transitions";
import { getImage } from "astro:assets";
const previewHref = new URL(
(await getImage({ src: preview, format: "png" })).src,
Astro.url,
).href;
const { title } = Astro.props;
---
<!doctype html>
@ -15,6 +17,7 @@ const previewHref = new URL(
<head>
<!-- Open Graph (Facebook, LinkedIn, Discord, Slack etc.) -->
<meta property="og:title" content={`${pkg.displayName} Deck`} />
<link rel="canonical" href={Astro.url.href} />
<meta property="og:description" content={pkg.description} />
<meta property="og:image" content={previewHref} />
<meta property="og:url" content={Astro.url} />
@ -34,7 +37,8 @@ const previewHref = new URL(
href="https://fonts.googleapis.com/css2?family=Alan+Sans:wght@300..900&display=swap"
rel="stylesheet"
/>
<title>Gameflow Deck</title>
<title>{title}</title>
<ClientRouter />
</head>
<body>
<slot />

View file

@ -1,4 +1,6 @@
---
import Dock from "../components/Dock.astro";
import Header from "../components/Header.astro";
import Welcome from "../components/Welcome.astro";
import Layout from "../layouts/Layout.astro";
@ -6,6 +8,8 @@ import Layout from "../layouts/Layout.astro";
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
---
<Layout>
<Layout title="Gameflow Deck">
<Header />
<Welcome />
<Dock />
</Layout>

93
src/pages/plugins.astro Normal file
View file

@ -0,0 +1,93 @@
---
import { Dot, Download, Puzzle } from "@lucide/astro";
import Header from "../components/Header.astro";
import Welcome from "../components/Welcome.astro";
import Layout from "../layouts/Layout.astro";
import { createHash } from "crypto"; // Node.js built-in
import { plugins } from "../scripts/getters";
import { Git, Npm } from "simple-icons-astro";
import Dock from "../components/Dock.astro";
function getGravatarUrl(email: string, size = 80) {
const hash = createHash("sha256")
.update(email.trim().toLowerCase())
.digest("hex");
return `https://gravatar.com/avatar/${hash}?s=${size}`;
}
---
<Layout title="Gameflow Deck - Plugins">
<Header />
<div class="divider">
<Puzzle size={48} />Plugins <span
class="bg-base-300 font-semibold px-2 rounded-2xl">{plugins.total}</span
>
</div>
<div class="p-8">
<ul class="grid grid-cols-1 md:grid-cols-2 gap-4">
{
plugins.objects.map((p) => {
const plugins = p;
const elements = [];
if (p.package.publisher) {
elements.push(
<div class="flex gap-2">
<img
class="rounded-full size-6"
src={getGravatarUrl(p.package.publisher?.email)}
/>
{p.package.publisher?.username}
</div>,
);
}
elements.push(p.package.version);
if (p.package.license) elements.push(p.package.license);
if (p.package.links.repository) {
elements.push(
<a title="Go to repository" href={p.package.links.repository}>
<Git size={18} />
</a>,
);
}
if (p.package.links.npm) {
elements.push(
<a
title={`Show ${p.package.name} on NPM`}
href={p.package.links.npm}
>
<Npm size={16} />
</a>,
);
}
return (
<li class="flex flex-wrap justify-between p-4 gap-2 bg-base-300 rounded-2xl">
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
<h1 class="font-bold text-2xl">{p.package.name}</h1>
</div>
<div class="text-base-content/60">{p.package.description}</div>
<ul class="flex flex-wrap gap-2">
{p.package.keywords?.map((k: string) => (
<li class="bg-base-100 px-2 rounded-full">{k}</li>
))}
</ul>
<div class="flex gap-1 items-center">
{elements.flatMap((item, i) =>
i === 0 ? [item] : [<Dot />, item],
)}
</div>
</div>
<div class="flex items-center">
<div class="bg-base-100 px-4 py-2 flex gap-2 rounded-3xl">
<Download />
{p.downloads.monthly}
</div>
</div>
</li>
);
})
}
</ul>
</div>
<Dock />
</Layout>

View file

@ -1,20 +1,22 @@
const githubHeaders = import.meta.env.GITHUB_TOKEN
? {
headers: {
Authorization: `Bearer ${import.meta.env.GITHUB_TOKEN}`,
},
}
headers: {
Authorization: `Bearer ${import.meta.env.GITHUB_TOKEN}`,
},
}
: {};
export const repoData = await fetch(
"https://api.github.com/repos/simeonradivoev/gameflow-deck",
githubHeaders,
)
.then((res) => {
.then((res) =>
{
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.catch((e) => {
.catch((e) =>
{
console.error(e);
return { stargazers_count: 0 };
});
@ -23,21 +25,25 @@ export const releaseData = await fetch(
"https://api.github.com/repos/simeonradivoev/gameflow-deck/releases/latest",
githubHeaders,
)
.then((res) => {
.then((res) =>
{
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.catch((e) => {
.catch((e) =>
{
console.error(e);
return { tag_name: "unknown" };
});
async function getTotalDownloads(owner: string, repo: string): Promise<number> {
async function getTotalDownloads (owner: string, repo: string): Promise<number>
{
let totalDownloads = 0;
let cursor: string | null = null;
let hasNextPage = true;
while (hasNextPage) {
while (hasNextPage)
{
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
@ -65,13 +71,15 @@ async function getTotalDownloads(owner: string, repo: string): Promise<number> {
}),
});
if (!res.ok) {
if (!res.ok)
{
throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
}
const json: any = await res.json();
if (json.errors) {
if (json.errors)
{
throw new Error(
`GraphQL error: ${json.errors.map((e: any) => e.message).join(", ")}`,
);
@ -79,14 +87,17 @@ async function getTotalDownloads(owner: string, repo: string): Promise<number> {
const releases = json.data?.repository?.releases;
if (!releases) {
if (!releases)
{
throw new Error(
`Repository ${owner}/${repo} not found or not accessible`,
);
}
for (const release of releases.nodes) {
for (const asset of release.releaseAssets.nodes) {
for (const release of releases.nodes)
{
for (const asset of release.releaseAssets.nodes)
{
totalDownloads += asset.downloadCount;
}
}
@ -101,7 +112,8 @@ async function getTotalDownloads(owner: string, repo: string): Promise<number> {
export const totalDownloads = await getTotalDownloads(
"simeonradivoev",
"gameflow-deck",
).catch((e) => {
).catch((e) =>
{
console.error(e);
return 0;
});
@ -110,11 +122,13 @@ export const appContributorsData = await fetch(
"https://api.github.com/repos/simeonradivoev/gameflow-deck/contributors",
githubHeaders,
)
.then((res) => {
.then((res) =>
{
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.catch((e) => {
.catch((e) =>
{
console.error(e);
return [];
});
@ -123,11 +137,13 @@ export const storeContributorsData = await fetch(
"https://api.github.com/repos/simeonradivoev/gameflow-store/contributors",
githubHeaders,
)
.then((res) => {
.then((res) =>
{
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.catch((e) => {
.catch((e) =>
{
console.error(e);
return [];
});
@ -137,7 +153,16 @@ export const emulators = await fetch(
)
.then((res) => res.json())
.then((d) => d.emulators as any[])
.catch((e) => {
.catch((e) =>
{
console.error(e);
return [] as any[];
});
export const plugins = await fetch('https://registry.npmjs.com/-/v1/search?text=keywords:gameflow-plugin')
.then(res => res.json())
.catch(e =>
{
console.error(e);
return [] as any[];
});