Adding Apps page filtering by platform. Fixes #256

This commit is contained in:
Dessalines 2023-10-31 21:09:31 -04:00
parent 17ecc71ba8
commit 9f51895a6b
8 changed files with 388 additions and 313 deletions

@ -1 +1 @@
Subproject commit 63e8c68ffc724a7e4e44de17c236c44d9b66d028 Subproject commit 35366c5ec0f198860bb860e4c7b5deda9e47636a

@ -1 +1 @@
Subproject commit ff8a2db505771c9f142ab31626b49de54a756192 Subproject commit 419d6e7c67951d1210226091b2d09a0536b531c8

View file

@ -9,6 +9,14 @@ export enum SourceType {
Open, Open,
} }
export enum Platform {
All = "all_platforms",
Android = "android",
IOS = "ios",
Web = "web",
CLI = "cli",
}
export interface AppDetails { export interface AppDetails {
name: string; name: string;
description: string; description: string;
@ -17,6 +25,7 @@ export interface AppDetails {
banner?: string; banner?: string;
links: AppLink[]; links: AppLink[];
sourceType: SourceType; sourceType: SourceType;
platforms: Platform[];
} }
export interface AppLink { export interface AppLink {
@ -43,7 +52,7 @@ export const API_LIBRARIES: ApiLibrary[] = [
}, },
]; ];
const voyagerApp: AppDetails = { const VOYAGER: AppDetails = {
name: "Voyager", name: "Voyager",
description: "A Lemmy Client for iOS, Android and the web", description: "A Lemmy Client for iOS, Android and the web",
link: "https://github.com/aeharding/voyager", link: "https://github.com/aeharding/voyager",
@ -64,9 +73,10 @@ const voyagerApp: AppDetails = {
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
platforms: [Platform.Android, Platform.IOS, Platform.Web],
}; };
const thunderApp: AppDetails = { const THUNDER: AppDetails = {
name: "Thunder", name: "Thunder",
description: description:
"An open-source cross-platform Lemmy client for iOS and Android built with Flutter", "An open-source cross-platform Lemmy client for iOS and Android built with Flutter",
@ -92,10 +102,10 @@ const thunderApp: AppDetails = {
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
platforms: [Platform.Android, Platform.IOS],
}; };
export const ANDROID_APPS: AppDetails[] = [ const JERBOA: AppDetails = {
{
name: "Jerboa", name: "Jerboa",
description: "A native Android app made by Lemmy's developers", description: "A native Android app made by Lemmy's developers",
link: "https://github.com/dessalines/jerboa", link: "https://github.com/dessalines/jerboa",
@ -116,8 +126,10 @@ export const ANDROID_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Android],
{ };
const ETERNITY: AppDetails = {
name: "Eternity", name: "Eternity",
description: "A Lemmy client for Android written in Java.", description: "A Lemmy client for Android written in Java.",
link: "https://codeberg.org/Bazsalanszky/Eternity", link: "https://codeberg.org/Bazsalanszky/Eternity",
@ -138,8 +150,10 @@ export const ANDROID_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Android],
{ };
const COMBUSTIBLE: AppDetails = {
name: "Combustible", name: "Combustible",
description: "An Open-Source Lemmy Client For Android", description: "An Open-Source Lemmy Client For Android",
link: "https://github.com/TheBrokenRail/Combustible", link: "https://github.com/TheBrokenRail/Combustible",
@ -156,8 +170,10 @@ export const ANDROID_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Android],
{ };
const LIFTOFF: AppDetails = {
name: "LiftOff!", name: "LiftOff!",
description: "A mobile client for lemmy", description: "A mobile client for lemmy",
link: "https://github.com/liftoff-app/liftoff", link: "https://github.com/liftoff-app/liftoff",
@ -178,10 +194,10 @@ export const ANDROID_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Android, Platform.IOS],
voyagerApp, };
thunderApp,
{ const BOOST: AppDetails = {
name: "Boost for Lemmy", name: "Boost for Lemmy",
description: "A smooth app for Lemmy.", description: "A smooth app for Lemmy.",
link: "https://play.google.com/store/apps/details?id=com.rubenmayayo.lemmy", link: "https://play.google.com/store/apps/details?id=com.rubenmayayo.lemmy",
@ -194,8 +210,10 @@ export const ANDROID_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Closed, sourceType: SourceType.Closed,
}, platforms: [Platform.Android],
{ };
const SYNC: AppDetails = {
name: "Sync for Lemmy", name: "Sync for Lemmy",
description: "A full-featured app for browsing Lemmy on the go.", description: "A full-featured app for browsing Lemmy on the go.",
link: "https://play.google.com/store/apps/details?id=io.syncapps.lemmy_sync", link: "https://play.google.com/store/apps/details?id=io.syncapps.lemmy_sync",
@ -208,11 +226,10 @@ export const ANDROID_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Closed, sourceType: SourceType.Closed,
}, platforms: [Platform.Android],
]; };
export const IOS_APPS: AppDetails[] = [ const MLEM: AppDetails = {
{
name: "Mlem", name: "Mlem",
description: "A Lemmy Client for iOS.", description: "A Lemmy Client for iOS.",
link: "https://github.com/mormaer/Mlem", link: "https://github.com/mormaer/Mlem",
@ -229,8 +246,10 @@ export const IOS_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.IOS],
{ };
const LUNAR: AppDetails = {
name: "Lunar", name: "Lunar",
description: "A Lemmy Client for iOS written in Swift and SwiftUI", description: "A Lemmy Client for iOS written in Swift and SwiftUI",
link: "https://github.com/mani-sh-reddy/Lunar", link: "https://github.com/mani-sh-reddy/Lunar",
@ -247,10 +266,10 @@ export const IOS_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.IOS],
voyagerApp, };
thunderApp,
{ const MEMMY: AppDetails = {
name: "Memmy", name: "Memmy",
description: description:
"A Lemmy Client built in React Native for iOS available on the App Store.", "A Lemmy Client built in React Native for iOS available on the App Store.",
@ -268,11 +287,10 @@ export const IOS_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.IOS],
]; };
export const WEB_APPS: AppDetails[] = [ const LEMMY_UI: AppDetails = {
{
name: "lemmy-ui", name: "lemmy-ui",
description: "The official web app for lemmy.", description: "The official web app for lemmy.",
link: "https://github.com/LemmyNet/lemmy-ui", link: "https://github.com/LemmyNet/lemmy-ui",
@ -284,8 +302,10 @@ export const WEB_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Web],
{ };
const PHOTON: AppDetails = {
name: "Photon", name: "Photon",
description: "A sleek lemmy web UI.", description: "A sleek lemmy web UI.",
link: "https://github.com/Xyphyn/photon", link: "https://github.com/Xyphyn/photon",
@ -298,8 +318,10 @@ export const WEB_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Web],
{ };
const ALEXANDRITE: AppDetails = {
name: "Alexandrite", name: "Alexandrite",
description: description:
"A beautiful and convenient desktop-first alternate web UI for Lemmy.", "A beautiful and convenient desktop-first alternate web UI for Lemmy.",
@ -313,8 +335,10 @@ export const WEB_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Web],
{ };
const MLMYM: AppDetails = {
name: "mlmym", name: "mlmym",
description: "A familiar desktop experience for lemmy", description: "A familiar desktop experience for lemmy",
link: "https://github.com/rystaf/mlmym", link: "https://github.com/rystaf/mlmym",
@ -326,8 +350,10 @@ export const WEB_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Web],
{ };
const LEMMYBB: AppDetails = {
name: "lemmyBB", name: "lemmyBB",
description: "A lemmy frontend based on phpBB.", description: "A lemmy frontend based on phpBB.",
link: "https://github.com/LemmyNet/lemmyBB", link: "https://github.com/LemmyNet/lemmyBB",
@ -339,11 +365,10 @@ export const WEB_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.Web],
]; };
export const CLI_APPS: AppDetails[] = [ const NEONMODEM: AppDetails = {
{
name: "neonmodem", name: "neonmodem",
description: "BBS-style TUI client", description: "BBS-style TUI client",
link: "https://github.com/mrusme/neonmodem", link: "https://github.com/mrusme/neonmodem",
@ -355,5 +380,25 @@ export const CLI_APPS: AppDetails[] = [
}, },
], ],
sourceType: SourceType.Open, sourceType: SourceType.Open,
}, platforms: [Platform.CLI],
};
export const APP_LIST: AppDetails[] = [
JERBOA,
ETERNITY,
COMBUSTIBLE,
LIFTOFF,
MLEM,
LUNAR,
MEMMY,
LEMMY_UI,
VOYAGER,
THUNDER,
PHOTON,
ALEXANDRITE,
MLMYM,
LEMMYBB,
NEONMODEM,
BOOST,
SYNC,
]; ];

View file

@ -1,22 +1,21 @@
import { Component } from "inferno"; import { Component, linkEvent } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next"; import { i18n } from "../i18next";
import { T } from "inferno-i18next"; import { T } from "inferno-i18next";
import { BottomSpacer, TEXT_GRADIENT } from "./common"; import { BottomSpacer, SELECT_CLASSES, TEXT_GRADIENT } from "./common";
import { import {
ANDROID_APPS,
API_LIBRARIES, API_LIBRARIES,
APP_LIST,
AppDetails, AppDetails,
AppLink, AppLink,
CLI_APPS, Platform,
IOS_APPS,
SourceType, SourceType,
WEB_APPS,
} from "./app-definitions"; } from "./app-definitions";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { I18nKeys } from "i18next";
const TitleBlock = () => ( const TitleBlock = () => (
<div className="flex flex-col items-center pt-16 mb-8"> <div className="flex flex-col items-center pt-16 mb-4">
<T i18nKey="lemmy_apps" className="text-4xl font-bold mb-3"> <T i18nKey="lemmy_apps" className="text-4xl font-bold mb-3">
#<span className={TEXT_GRADIENT}>#</span> #<span className={TEXT_GRADIENT}>#</span>
</T> </T>
@ -83,29 +82,6 @@ const AppTitle = ({ title }) => (
<div className="text-2xl mb-3 text-gray-300">{title}</div> <div className="text-2xl mb-3 text-gray-300">{title}</div>
); );
const MobileAppsBlock = () => (
<div>
<AppTitle title={i18n.t("mobile_apps_for_android")} />
<AppGrid apps={ANDROID_APPS} />
<AppTitle title={i18n.t("mobile_apps_for_ios")} />
<AppGrid apps={IOS_APPS} />
</div>
);
const WebAppsBlock = () => (
<div>
<AppTitle title={i18n.t("web_apps")} />
<AppGrid apps={WEB_APPS} />
</div>
);
const CliAppsBlock = () => (
<div>
<AppTitle title={i18n.t("cli_apps")} />
<AppGrid apps={CLI_APPS} />
</div>
);
interface AppGridProps { interface AppGridProps {
apps: AppDetails[]; apps: AppDetails[];
} }
@ -142,13 +118,35 @@ const ApiLibrariesBlock = () => (
</div> </div>
); );
export class Apps extends Component<any, any> { interface State {
apps: AppDetails[];
platform: Platform;
}
export class Apps extends Component<any, State> {
state: State = {
apps: [],
platform: Platform.All,
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
componentDidMount() { componentDidMount() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.buildAppList();
}
buildAppList() {
let apps = APP_LIST;
// Platform filter
if (this.state.platform !== Platform.All) {
apps = apps.filter(a => a.platforms.includes(this.state.platform));
}
this.setState({ apps });
} }
render() { render() {
@ -159,12 +157,44 @@ export class Apps extends Component<any, any> {
<meta property={"title"} content={title} /> <meta property={"title"} content={title} />
</Helmet> </Helmet>
<TitleBlock /> <TitleBlock />
<MobileAppsBlock /> {this.filterAndTitleBlock()}
<WebAppsBlock /> <AppGrid apps={this.state.apps} />
<CliAppsBlock />
<ApiLibrariesBlock /> <ApiLibrariesBlock />
<BottomSpacer /> <BottomSpacer />
</div> </div>
); );
} }
filterAndTitleBlock() {
return (
<div id="search">
<div className="flex flex-row flex-wrap gap-4 mb-4">
<div className="flex-none"></div>
<div className="grow"></div>
<div>
<select
className={`${SELECT_CLASSES} mr-2`}
value={this.state.platform}
onChange={linkEvent(this, handlePlatformChange)}
name="platform_select"
>
{Object.entries(Platform).map(([name, platform]) => (
<option key={name} value={platform}>
{i18n.t(platform as string as I18nKeys)}
</option>
))}
</select>
</div>
</div>
</div>
);
}
}
function handlePlatformChange(i: Apps, event: any) {
let platform: Platform = (event.target.value as Platform) ?? Platform.All;
i.setState({
platform,
});
i.buildAppList();
} }

View file

@ -142,3 +142,7 @@ export const DonateBlock = () => (
); );
export const BottomSpacer = () => <div className="pb-32" />; export const BottomSpacer = () => <div className="pb-32" />;
export const SectionTitle = ({ title }) => (
<div className="text-2xl mb-3">{title}</div>
);

View file

@ -1,6 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { All_TOPIC, TOPICS, Topic } from "./instances-definitions"; import { ALL_TOPIC, TOPICS, Topic } from "./instances-definitions";
import { LANGUAGES, i18n } from "../i18next"; import { LANGUAGES, i18n } from "../i18next";
import { I18nKeys } from "i18next"; import { I18nKeys } from "i18next";
import { Icon } from "./icon"; import { Icon } from "./icon";
@ -120,14 +120,14 @@ export class InstancePicker extends Component<Props, State> {
function handleTopicChange(i: InstancePicker, event: any) { function handleTopicChange(i: InstancePicker, event: any) {
i.setState({ i.setState({
topic: TOPICS.find(c => c.name == event.target.value) ?? All_TOPIC, topic: TOPICS.find(c => c.name == event.target.value) ?? ALL_TOPIC,
activeStep: Step.Language, activeStep: Step.Language,
}); });
} }
function handleLanguageChange(i: InstancePicker, event: any) { function handleLanguageChange(i: InstancePicker, event: any) {
i.setState({ language: event.target.value }); i.setState({ language: event.target.value });
const url = `/instances?topic=${i.state.topic?.name ?? All_TOPIC}&language=${ const url = `/instances?topic=${i.state.topic?.name ?? ALL_TOPIC}&language=${
i.state.language i.state.language
}&scroll=true`; }&scroll=true`;

View file

@ -27,7 +27,7 @@ export interface Topic {
icon: string; icon: string;
} }
export const All_TOPIC: Topic = { export const ALL_TOPIC: Topic = {
name: "all_topics", name: "all_topics",
icon: "folder", icon: "folder",
}; };
@ -88,7 +88,7 @@ const SPORTS: Topic = {
}; };
export const TOPICS: Topic[] = [ export const TOPICS: Topic[] = [
All_TOPIC, ALL_TOPIC,
GENERAL, GENERAL,
TECHNOLOGY, TECHNOLOGY,
POLITICS, POLITICS,

View file

@ -4,12 +4,12 @@ import { i18n, LANGUAGES } from "../i18next";
import { T } from "inferno-i18next"; import { T } from "inferno-i18next";
import { instance_stats } from "../instance_stats"; import { instance_stats } from "../instance_stats";
import { getQueryParams, mdToHtml, numToSI } from "../utils"; import { getQueryParams, mdToHtml, numToSI } from "../utils";
import { Badge, SELECT_CLASSES, TEXT_GRADIENT } from "./common"; import { Badge, SELECT_CLASSES, SectionTitle, TEXT_GRADIENT } from "./common";
import { import {
INSTANCE_HELPERS, INSTANCE_HELPERS,
Topic, Topic,
RECOMMENDED_INSTANCES, RECOMMENDED_INSTANCES,
All_TOPIC, ALL_TOPIC,
TOPICS, TOPICS,
} from "./instances-definitions"; } from "./instances-definitions";
import { Icon, IconSize } from "./icon"; import { Icon, IconSize } from "./icon";
@ -80,10 +80,6 @@ const ComparisonBlock = () => (
</div> </div>
); );
const SectionTitle = ({ title }) => (
<div className="text-2xl mb-3">{title}</div>
);
interface InstanceCardGridProps { interface InstanceCardGridProps {
title: string; title: string;
instances: any[]; instances: any[];
@ -367,7 +363,7 @@ function getSortFromQuery(sort?: string): Sort {
} }
function getTopicFromQuery(topic?: string): Topic { function getTopicFromQuery(topic?: string): Topic {
return TOPICS.find(c => c.name == topic) ?? All_TOPIC; return TOPICS.find(c => c.name == topic) ?? ALL_TOPIC;
} }
function getInstancesQueryParams() { function getInstancesQueryParams() {
@ -384,7 +380,7 @@ export class Instances extends Component<Props, State> {
instances: [], instances: [],
sort: RANDOM_SORT, sort: RANDOM_SORT,
language: "all", language: "all",
topic: All_TOPIC, topic: ALL_TOPIC,
scroll: false, scroll: false,
}; };
@ -434,7 +430,7 @@ export class Instances extends Component<Props, State> {
} }
// Topic filter // Topic filter
if (this.state.topic !== All_TOPIC) { if (this.state.topic !== ALL_TOPIC) {
const topicRecs = recommended.filter(r => const topicRecs = recommended.filter(r =>
r.topics.includes(this.state.topic), r.topics.includes(this.state.topic),
); );
@ -565,7 +561,7 @@ function handleSortChange(i: Instances, event: any) {
function handleTopicChange(i: Instances, event: any) { function handleTopicChange(i: Instances, event: any) {
i.setState({ i.setState({
topic: TOPICS.find(c => c.name == event.target.value) ?? All_TOPIC, topic: TOPICS.find(c => c.name == event.target.value) ?? ALL_TOPIC,
}); });
i.buildInstanceList(); i.buildInstanceList();
} }
@ -579,7 +575,7 @@ function handleSeeAll(i: Instances) {
i.setState({ i.setState({
sort: RANDOM_SORT, sort: RANDOM_SORT,
language: "all", language: "all",
topic: All_TOPIC, topic: ALL_TOPIC,
}); });
i.buildInstanceList(); i.buildInstanceList();
} }