this repo has no description

Merge pull request #10 from redstonekasi/mb-filtering

Moonbase extension filtering

Changed files
+460 -35
packages
core-extensions
types
src
coreExtensions
discord
+2 -1
packages/core-extensions/src/moonbase/index.tsx
···
TrashIconSVG,
CircleXIconSVG,
"Masks.PANEL_BUTTON",
-
"removeButtonContainer:"
+
"removeButtonContainer:",
+
'"Missing channel in Channel.openChannelContextMenu"'
],
entrypoint: true,
run: (module, exports, require) => {
+6
packages/core-extensions/src/moonbase/types.ts
···
"M7.02799 0.333252C3.346 0.333252 0.361328 3.31792 0.361328 6.99992C0.361328 10.6819 3.346 13.6666 7.02799 13.6666C10.71 13.6666 13.6947 10.6819 13.6947 6.99992C13.6947 3.31792 10.7093 0.333252 7.02799 0.333252ZM10.166 9.19525L9.22333 10.1379L7.02799 7.94325L4.83266 10.1379L3.89 9.19525L6.08466 6.99992L3.88933 4.80459L4.832 3.86259L7.02733 6.05792L9.22266 3.86259L10.1653 4.80459L7.97066 6.99992L10.166 9.19525Z";
export const DangerIconSVG =
"M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm1.44-15.94L13.06 14a1.06 1.06 0 0 1-2.12 0l-.38-6.94a1 1 0 0 1 1-1.06h.88a1 1 0 0 1 1 1.06Zm-.19 10.69a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z";
+
export const ChevronSmallDownIconSVG =
+
"M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z";
+
export const ChevronSmallUpIconSVG =
+
"M7.41 16.0001L12 11.4201L16.59 16.0001L18 14.5901L12 8.59006L6 14.5901L7.41 16.0001Z";
+
export const ArrowsUpDownIconSVG =
+
"M3.81962 11.3333L3.81962 1.33325L5.52983 1.33325L5.52985 11.3333L7.46703 9.36658L8.66663 10.5916L4.67068 14.6666L0.666626 10.5916L1.86622 9.34158L3.81962 11.3333Z";
export type MoonbaseNatives = {
fetchRepositories(
+378
packages/core-extensions/src/moonbase/ui/filterBar.tsx
···
+
import { WebpackRequireType } from "@moonlight-mod/types";
+
import { tagNames } from "./info";
+
import {
+
ArrowsUpDownIconSVG,
+
ChevronSmallDownIconSVG,
+
ChevronSmallUpIconSVG
+
} from "../types";
+
+
export const defaultFilter = {
+
core: true,
+
normal: true,
+
developer: true,
+
enabled: true,
+
disabled: true,
+
installed: true,
+
repository: true
+
};
+
export type Filter = typeof defaultFilter;
+
+
export default async (require: WebpackRequireType) => {
+
const spacepack = require("spacepack_spacepack").spacepack;
+
const React = require("common_react");
+
const Flux = require("common_flux");
+
const { WindowStore } = require("common_stores");
+
+
const {
+
Button,
+
Text,
+
Heading,
+
Popout,
+
Dialog
+
} = require("common_components");
+
+
const channelModule =
+
require.m[
+
spacepack.findByCode(
+
'"Missing channel in Channel.openChannelContextMenu"'
+
)[0].id
+
].toString();
+
const moduleId = channelModule.match(/webpackId:"(.+?)"/)![1];
+
await require.el(moduleId);
+
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
+
const SortMenuClasses = spacepack.findByCode("container:", "clearText:")[0]
+
.exports;
+
const FilterDialogClasses = spacepack.findByCode(
+
"countContainer:",
+
"tagContainer:"
+
)[0].exports;
+
const FilterBarClasses = spacepack.findByCode("tagsButtonWithCount:")[0]
+
.exports;
+
+
const TagItem = spacepack.findByCode("IncreasedActivityForumTagPill:")[0]
+
.exports.default;
+
+
const ChevronSmallDownIcon = spacepack.findByCode(ChevronSmallDownIconSVG)[0]
+
.exports.default;
+
const ChevronSmallUpIcon = spacepack.findByCode(ChevronSmallUpIconSVG)[0]
+
.exports.default;
+
const ArrowsUpDownIcon =
+
spacepack.findByCode(ArrowsUpDownIconSVG)[0].exports.default;
+
+
function toggleTag(
+
selectedTags: Set<string>,
+
setSelectedTags: (tags: Set<string>) => void,
+
tag: string
+
) {
+
const newState = new Set(selectedTags);
+
if (newState.has(tag)) newState.delete(tag);
+
else newState.add(tag);
+
setSelectedTags(newState);
+
}
+
+
function FilterButtonPopout({
+
filter,
+
setFilter,
+
closePopout
+
}: {
+
filter: Filter;
+
setFilter: (filter: Filter) => void;
+
closePopout: () => void;
+
}) {
+
const {
+
Menu,
+
MenuItem,
+
MenuGroup,
+
MenuCheckboxItem
+
} = require("common_components");
+
+
return (
+
<div className={SortMenuClasses.container}>
+
<Menu navId="sort-filter" hideScrollbar={true} onClose={closePopout}>
+
<MenuGroup label="Type">
+
<MenuCheckboxItem
+
id="t-core"
+
label="Core"
+
checked={filter.core}
+
action={() => setFilter({ ...filter, core: !filter.core })}
+
/>
+
<MenuCheckboxItem
+
id="t-normal"
+
label="Normal"
+
checked={filter.normal}
+
action={() => setFilter({ ...filter, normal: !filter.normal })}
+
/>
+
<MenuCheckboxItem
+
id="t-developer"
+
label="Developer"
+
checked={filter.developer}
+
action={() =>
+
setFilter({ ...filter, developer: !filter.developer })
+
}
+
/>
+
</MenuGroup>
+
<MenuGroup label="State">
+
<MenuCheckboxItem
+
id="s-enabled"
+
label="Enabled"
+
checked={filter.enabled}
+
action={() => setFilter({ ...filter, enabled: !filter.enabled })}
+
/>
+
<MenuCheckboxItem
+
id="s-disabled"
+
label="Disabled"
+
checked={filter.disabled}
+
action={() =>
+
setFilter({ ...filter, disabled: !filter.disabled })
+
}
+
/>
+
</MenuGroup>
+
<MenuGroup label="Location">
+
<MenuCheckboxItem
+
id="l-installed"
+
label="Installed"
+
checked={filter.installed}
+
action={() =>
+
setFilter({ ...filter, installed: !filter.installed })
+
}
+
/>
+
<MenuCheckboxItem
+
id="l-repository"
+
label="Repository"
+
checked={filter.repository}
+
action={() =>
+
setFilter({ ...filter, repository: !filter.repository })
+
}
+
/>
+
</MenuGroup>
+
<MenuGroup>
+
<MenuItem
+
id="reset-all"
+
className={SortMenuClasses.clearText}
+
label={
+
<Text variant="text-sm/medium" color="none">
+
Reset to default
+
</Text>
+
}
+
action={() => {
+
setFilter({ ...defaultFilter });
+
closePopout();
+
}}
+
/>
+
</MenuGroup>
+
</Menu>
+
</div>
+
);
+
}
+
+
function TagButtonPopout({
+
selectedTags,
+
setSelectedTags,
+
setPopoutRef,
+
closePopout
+
}: any) {
+
return (
+
<Dialog ref={setPopoutRef} className={FilterDialogClasses.container}>
+
<div className={FilterDialogClasses.header}>
+
<div className={FilterDialogClasses.headerLeft}>
+
<Heading
+
color="interactive-normal"
+
variant="text-xs/bold"
+
className={FilterDialogClasses.headerText}
+
>
+
Select tags
+
</Heading>
+
<div className={FilterDialogClasses.countContainer}>
+
<Text
+
className={FilterDialogClasses.countText}
+
color="none"
+
variant="text-xs/medium"
+
>
+
{selectedTags.size}
+
</Text>
+
</div>
+
</div>
+
</div>
+
<div className={FilterDialogClasses.tagContainer}>
+
{Object.keys(tagNames).map((tag) => (
+
<TagItem
+
key={tag}
+
className={FilterDialogClasses.tag}
+
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
+
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
+
selected={selectedTags.has(tag)}
+
/>
+
))}
+
</div>
+
<div className={FilterDialogClasses.separator} />
+
<Button
+
look={Button.Looks.LINK}
+
size={Button.Sizes.MIN}
+
color={Button.Colors.CUSTOM}
+
className={FilterDialogClasses.clear}
+
onClick={() => {
+
setSelectedTags(new Set());
+
closePopout();
+
}}
+
>
+
<Text variant="text-sm/medium" color="text-link">
+
Clear all
+
</Text>
+
</Button>
+
</Dialog>
+
);
+
}
+
+
return function FilterBar({
+
filter,
+
setFilter,
+
selectedTags,
+
setSelectedTags
+
}: {
+
filter: Filter;
+
setFilter: (filter: Filter) => void;
+
selectedTags: Set<string>;
+
setSelectedTags: (tags: Set<string>) => void;
+
}) {
+
const windowSize = Flux.useStateFromStores([WindowStore], () =>
+
WindowStore.windowSize()
+
);
+
+
const tagsContainer = React.useRef<HTMLDivElement>(null);
+
const tagListInner = React.useRef<HTMLDivElement>(null);
+
const [tagsButtonOffset, setTagsButtonOffset] = React.useState(0);
+
React.useLayoutEffect(() => {
+
if (tagsContainer.current === null || tagListInner.current === null)
+
return;
+
const { left: containerX, top: containerY } =
+
tagsContainer.current.getBoundingClientRect();
+
let offset = 0;
+
for (const child of tagListInner.current.children) {
+
const {
+
right: childX,
+
top: childY,
+
height
+
} = child.getBoundingClientRect();
+
if (childY - containerY > height) break;
+
const newOffset = childX - containerX;
+
if (newOffset > offset) {
+
offset = newOffset;
+
}
+
}
+
setTagsButtonOffset(offset);
+
}, [windowSize]);
+
+
return (
+
<div
+
ref={tagsContainer}
+
style={{
+
paddingTop: "12px"
+
}}
+
className={`${FilterBarClasses.tagsContainer} ${Margins.marginBottom8}`}
+
>
+
<Popout
+
renderPopout={({ closePopout }: any) => (
+
<FilterButtonPopout
+
filter={filter}
+
setFilter={setFilter}
+
closePopout={closePopout}
+
/>
+
)}
+
position="bottom"
+
align="left"
+
>
+
{(props: any, { isShown }: { isShown: boolean }) => (
+
<Button
+
{...props}
+
size={Button.Sizes.MIN}
+
color={Button.Colors.CUSTOM}
+
className={FilterBarClasses.sortDropdown}
+
innerClassName={FilterBarClasses.sortDropdownInner}
+
>
+
<ArrowsUpDownIcon />
+
<Text
+
className={FilterBarClasses.sortDropdownText}
+
variant="text-sm/medium"
+
color="interactive-normal"
+
>
+
Sort & filter
+
</Text>
+
{isShown ? (
+
<ChevronSmallUpIcon size={20} />
+
) : (
+
<ChevronSmallDownIcon size={20} />
+
)}
+
</Button>
+
)}
+
</Popout>
+
<div className={FilterBarClasses.divider} />
+
<div className={FilterBarClasses.tagList}>
+
<div ref={tagListInner} className={FilterBarClasses.tagListInner}>
+
{Object.keys(tagNames).map((tag) => (
+
<TagItem
+
key={tag}
+
className={FilterBarClasses.tag}
+
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
+
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
+
selected={selectedTags.has(tag)}
+
/>
+
))}
+
</div>
+
</div>
+
<Popout
+
renderPopout={({ setPopoutRef, closePopout }: any) => (
+
<TagButtonPopout
+
selectedTags={selectedTags}
+
setSelectedTags={setSelectedTags}
+
setPopoutRef={setPopoutRef}
+
closePopout={closePopout}
+
/>
+
)}
+
position="bottom"
+
align="right"
+
>
+
{(props: any, { isShown }: { isShown: boolean }) => (
+
<Button
+
{...props}
+
size={Button.Sizes.MIN}
+
color={Button.Colors.CUSTOM}
+
style={{
+
left: tagsButtonOffset
+
}}
+
// TODO: Use Discord's class name utility
+
className={`${FilterBarClasses.tagsButton} ${
+
selectedTags.size > 0
+
? FilterBarClasses.tagsButtonWithCount
+
: ""
+
}`}
+
innerClassName={FilterBarClasses.tagsButtonInner}
+
>
+
{selectedTags.size > 0 ? (
+
<div
+
style={{ boxSizing: "content-box" }}
+
className={FilterBarClasses.countContainer}
+
>
+
<Text
+
className={FilterBarClasses.countText}
+
color="none"
+
variant="text-xs/medium"
+
>
+
{selectedTags.size}
+
</Text>
+
</div>
+
) : (
+
<>All</>
+
)}
+
{isShown ? (
+
<ChevronSmallUpIcon size={20} />
+
) : (
+
<ChevronSmallDownIcon size={20} />
+
)}
+
</Button>
+
)}
+
</Popout>
+
</div>
+
);
+
};
+
};
+45 -13
packages/core-extensions/src/moonbase/ui/index.tsx
···
-
import WebpackRequire from "@moonlight-mod/types/discord/require";
+
import {
+
ExtensionLoadSource,
+
ExtensionTag,
+
WebpackRequireType
+
} from "@moonlight-mod/types";
import card from "./card";
+
import filterBar, { defaultFilter } from "./filterBar";
+
import { ExtensionState } from "../types";
export enum ExtensionPage {
Info,
···
Settings
}
-
export default (require: typeof WebpackRequire) => {
+
export default (require: WebpackRequireType) => {
const React = require("common_react");
const spacepack = require("spacepack_spacepack").spacepack;
const Flux = require("common_flux");
···
require("moonbase_stores") as typeof import("../webpackModules/stores");
const ExtensionCard = card(require);
+
const FilterBar = React.lazy(() =>
+
filterBar(require).then((c) => ({ default: c }))
+
);
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
const SearchBar = spacepack.findByCode("Messages.SEARCH", "hideSearchIcon")[0]
···
);
const [query, setQuery] = React.useState("");
+
const [filter, setFilter] = React.useState({ ...defaultFilter });
+
const [selectedTags, setSelectedTags] = React.useState(new Set<string>());
const sorted = Object.values(extensions).sort((a, b) => {
const aName = a.manifest.meta?.name ?? a.id;
···
return aName.localeCompare(bName);
});
-
const filtered = query.trim().length
-
? sorted.filter(
-
(ext) =>
-
ext.manifest.meta?.name?.toLowerCase().includes(query) ||
-
ext.manifest.meta?.tagline?.toLowerCase().includes(query) ||
-
ext.manifest.meta?.description?.toLowerCase().includes(query)
+
const filtered = sorted.filter(
+
(ext) =>
+
(ext.manifest.meta?.name?.toLowerCase().includes(query) ||
+
ext.manifest.meta?.tagline?.toLowerCase().includes(query) ||
+
ext.manifest.meta?.description?.toLowerCase().includes(query)) &&
+
[...selectedTags.values()].every(
+
(tag) => ext.manifest.meta?.tags?.includes(tag as ExtensionTag)
+
) &&
+
// This seems very bad, sorry
+
!(
+
(!filter.core && ext.source.type === ExtensionLoadSource.Core) ||
+
(!filter.normal && ext.source.type === ExtensionLoadSource.Normal) ||
+
(!filter.developer &&
+
ext.source.type === ExtensionLoadSource.Developer) ||
+
(!filter.enabled &&
+
MoonbaseSettingsStore.getExtensionEnabled(ext.id)) ||
+
(!filter.disabled &&
+
!MoonbaseSettingsStore.getExtensionEnabled(ext.id)) ||
+
(!filter.installed && ext.state !== ExtensionState.NotDownloaded) ||
+
(!filter.repository && ext.state === ExtensionState.NotDownloaded)
)
-
: sorted;
+
);
return (
<>
<Text
-
style={{
-
"margin-bottom": "16px"
-
}}
+
className={Margins.marginBottom20}
variant="heading-lg/semibold"
tag="h2"
>
···
</Text>
<SearchBar
size={SearchBar.Sizes.MEDIUM}
-
className={Margins.marginBottom20}
query={query}
onChange={(v: string) => setQuery(v.toLowerCase())}
onClear={() => setQuery("")}
···
spellCheck: "false"
}}
/>
+
<React.Suspense
+
fallback={<div className={Margins.marginBottom20}></div>}
+
>
+
<FilterBar
+
filter={filter}
+
setFilter={setFilter}
+
selectedTags={selectedTags}
+
setSelectedTags={setSelectedTags}
+
/>
+
</React.Suspense>
{filtered.map((ext) => (
<ExtensionCard id={ext.id} key={ext.id} />
))}
+19 -18
packages/core-extensions/src/moonbase/ui/info.tsx
···
Incompatible = "incompatible"
}
+
export const tagNames: Record<ExtensionTag, string> = {
+
[ExtensionTag.Accessibility]: "Accessibility",
+
[ExtensionTag.Appearance]: "Appearance",
+
[ExtensionTag.Chat]: "Chat",
+
[ExtensionTag.Commands]: "Commands",
+
[ExtensionTag.ContextMenu]: "Context Menu",
+
[ExtensionTag.DangerZone]: "Danger Zone",
+
[ExtensionTag.Development]: "Development",
+
[ExtensionTag.Fixes]: "Fixes",
+
[ExtensionTag.Fun]: "Fun",
+
[ExtensionTag.Markdown]: "Markdown",
+
[ExtensionTag.Voice]: "Voice",
+
[ExtensionTag.Privacy]: "Privacy",
+
[ExtensionTag.Profiles]: "Profiles",
+
[ExtensionTag.QualityOfLife]: "Quality of Life",
+
[ExtensionTag.Library]: "Library"
+
};
+
export default (require: typeof WebpackRequire) => {
const React = require("common_react");
const spacepack = require("spacepack_spacepack").spacepack;
···
{tags != null && (
<InfoSection title="Tags">
{tags.map((tag, i) => {
-
const names: Record<ExtensionTag, string> = {
-
[ExtensionTag.Accessibility]: "Accessibility",
-
[ExtensionTag.Appearance]: "Appearance",
-
[ExtensionTag.Chat]: "Chat",
-
[ExtensionTag.Commands]: "Commands",
-
[ExtensionTag.ContextMenu]: "Context Menu",
-
[ExtensionTag.DangerZone]: "Danger Zone",
-
[ExtensionTag.Development]: "Development",
-
[ExtensionTag.Fixes]: "Fixes",
-
[ExtensionTag.Fun]: "Fun",
-
[ExtensionTag.Markdown]: "Markdown",
-
[ExtensionTag.Voice]: "Voice",
-
[ExtensionTag.Privacy]: "Privacy",
-
[ExtensionTag.Profiles]: "Profiles",
-
[ExtensionTag.QualityOfLife]: "Quality of Life",
-
[ExtensionTag.Library]: "Library"
-
};
-
const name = names[tag];
+
const name = tagNames[tag];
return (
<Badge
+2 -3
packages/types/src/coreExtensions.ts
···
import { CommonComponents as CommonComponents_ } from "./coreExtensions/components";
import { Dispatcher } from "flux";
import React from "react";
-
import { WebpackModuleFunc } from "./discord";
-
import WebpackRequire from "./discord/require";
+
import { WebpackModuleFunc, WebpackRequireType } from "./discord";
export type Spacepack = {
inspect: (module: number | string) => WebpackModuleFunc | null;
findByCode: (...args: (string | RegExp)[]) => any[];
findByExports: (...args: string[]) => any[];
-
require: typeof WebpackRequire;
+
require: WebpackRequireType;
modules: Record<string, WebpackModuleFunc>;
cache: Record<string, any>;
findObjectFromKey: (exports: Record<string, any>, key: string) => any | null;
+7
packages/types/src/coreExtensions/components.ts
···
Avatar: Component;
Scroller: Component;
Text: ComponentClass<PropsWithChildren<any>>;
+
Heading: ComponentClass<PropsWithChildren<any>>;
LegacyText: Component;
Flex: Flex;
Card: ComponentClass<PropsWithChildren<any>>;
+
Popout: ComponentClass<PropsWithChildren<any>>;
+
Dialog: ComponentClass<PropsWithChildren<any>>;
+
Menu: ComponentClass<PropsWithChildren<any>>;
+
MenuItem: ComponentClass<PropsWithChildren<any>>;
+
MenuGroup: ComponentClass<PropsWithChildren<any>>;
+
MenuCheckboxItem: ComponentClass<PropsWithChildren<any>>;
CardClasses: {
card: string;
cardHeader: string;
+1
packages/types/src/discord/webpack.ts
···
export type WebpackRequireType = typeof WebpackRequire & {
c: Record<string, WebpackModule>;
m: Record<string, WebpackModuleFunc>;
+
el: (module: number | string) => Promise<void>;
};
export type WebpackModule = {