+2
-1
packages/core-extensions/src/moonbase/index.tsx
+2
-1
packages/core-extensions/src/moonbase/index.tsx
+6
packages/core-extensions/src/moonbase/types.ts
+6
packages/core-extensions/src/moonbase/types.ts
···
8
8
"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";
9
9
export const DangerIconSVG =
10
10
"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";
11
+
export const ChevronSmallDownIconSVG =
12
+
"M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z";
13
+
export const ChevronSmallUpIconSVG =
14
+
"M7.41 16.0001L12 11.4201L16.59 16.0001L18 14.5901L12 8.59006L6 14.5901L7.41 16.0001Z";
15
+
export const ArrowsUpDownIconSVG =
16
+
"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";
11
17
12
18
export type MoonbaseNatives = {
13
19
fetchRepositories(
+378
packages/core-extensions/src/moonbase/ui/filterBar.tsx
+378
packages/core-extensions/src/moonbase/ui/filterBar.tsx
···
1
+
import { WebpackRequireType } from "@moonlight-mod/types";
2
+
import { tagNames } from "./info";
3
+
import {
4
+
ArrowsUpDownIconSVG,
5
+
ChevronSmallDownIconSVG,
6
+
ChevronSmallUpIconSVG
7
+
} from "../types";
8
+
9
+
export const defaultFilter = {
10
+
core: true,
11
+
normal: true,
12
+
developer: true,
13
+
enabled: true,
14
+
disabled: true,
15
+
installed: true,
16
+
repository: true
17
+
};
18
+
export type Filter = typeof defaultFilter;
19
+
20
+
export default async (require: WebpackRequireType) => {
21
+
const spacepack = require("spacepack_spacepack").spacepack;
22
+
const React = require("common_react");
23
+
const Flux = require("common_flux");
24
+
const { WindowStore } = require("common_stores");
25
+
26
+
const {
27
+
Button,
28
+
Text,
29
+
Heading,
30
+
Popout,
31
+
Dialog
32
+
} = require("common_components");
33
+
34
+
const channelModule =
35
+
require.m[
36
+
spacepack.findByCode(
37
+
'"Missing channel in Channel.openChannelContextMenu"'
38
+
)[0].id
39
+
].toString();
40
+
const moduleId = channelModule.match(/webpackId:"(.+?)"/)![1];
41
+
await require.el(moduleId);
42
+
43
+
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
44
+
const SortMenuClasses = spacepack.findByCode("container:", "clearText:")[0]
45
+
.exports;
46
+
const FilterDialogClasses = spacepack.findByCode(
47
+
"countContainer:",
48
+
"tagContainer:"
49
+
)[0].exports;
50
+
const FilterBarClasses = spacepack.findByCode("tagsButtonWithCount:")[0]
51
+
.exports;
52
+
53
+
const TagItem = spacepack.findByCode("IncreasedActivityForumTagPill:")[0]
54
+
.exports.default;
55
+
56
+
const ChevronSmallDownIcon = spacepack.findByCode(ChevronSmallDownIconSVG)[0]
57
+
.exports.default;
58
+
const ChevronSmallUpIcon = spacepack.findByCode(ChevronSmallUpIconSVG)[0]
59
+
.exports.default;
60
+
const ArrowsUpDownIcon =
61
+
spacepack.findByCode(ArrowsUpDownIconSVG)[0].exports.default;
62
+
63
+
function toggleTag(
64
+
selectedTags: Set<string>,
65
+
setSelectedTags: (tags: Set<string>) => void,
66
+
tag: string
67
+
) {
68
+
const newState = new Set(selectedTags);
69
+
if (newState.has(tag)) newState.delete(tag);
70
+
else newState.add(tag);
71
+
setSelectedTags(newState);
72
+
}
73
+
74
+
function FilterButtonPopout({
75
+
filter,
76
+
setFilter,
77
+
closePopout
78
+
}: {
79
+
filter: Filter;
80
+
setFilter: (filter: Filter) => void;
81
+
closePopout: () => void;
82
+
}) {
83
+
const {
84
+
Menu,
85
+
MenuItem,
86
+
MenuGroup,
87
+
MenuCheckboxItem
88
+
} = require("common_components");
89
+
90
+
return (
91
+
<div className={SortMenuClasses.container}>
92
+
<Menu navId="sort-filter" hideScrollbar={true} onClose={closePopout}>
93
+
<MenuGroup label="Type">
94
+
<MenuCheckboxItem
95
+
id="t-core"
96
+
label="Core"
97
+
checked={filter.core}
98
+
action={() => setFilter({ ...filter, core: !filter.core })}
99
+
/>
100
+
<MenuCheckboxItem
101
+
id="t-normal"
102
+
label="Normal"
103
+
checked={filter.normal}
104
+
action={() => setFilter({ ...filter, normal: !filter.normal })}
105
+
/>
106
+
<MenuCheckboxItem
107
+
id="t-developer"
108
+
label="Developer"
109
+
checked={filter.developer}
110
+
action={() =>
111
+
setFilter({ ...filter, developer: !filter.developer })
112
+
}
113
+
/>
114
+
</MenuGroup>
115
+
<MenuGroup label="State">
116
+
<MenuCheckboxItem
117
+
id="s-enabled"
118
+
label="Enabled"
119
+
checked={filter.enabled}
120
+
action={() => setFilter({ ...filter, enabled: !filter.enabled })}
121
+
/>
122
+
<MenuCheckboxItem
123
+
id="s-disabled"
124
+
label="Disabled"
125
+
checked={filter.disabled}
126
+
action={() =>
127
+
setFilter({ ...filter, disabled: !filter.disabled })
128
+
}
129
+
/>
130
+
</MenuGroup>
131
+
<MenuGroup label="Location">
132
+
<MenuCheckboxItem
133
+
id="l-installed"
134
+
label="Installed"
135
+
checked={filter.installed}
136
+
action={() =>
137
+
setFilter({ ...filter, installed: !filter.installed })
138
+
}
139
+
/>
140
+
<MenuCheckboxItem
141
+
id="l-repository"
142
+
label="Repository"
143
+
checked={filter.repository}
144
+
action={() =>
145
+
setFilter({ ...filter, repository: !filter.repository })
146
+
}
147
+
/>
148
+
</MenuGroup>
149
+
<MenuGroup>
150
+
<MenuItem
151
+
id="reset-all"
152
+
className={SortMenuClasses.clearText}
153
+
label={
154
+
<Text variant="text-sm/medium" color="none">
155
+
Reset to default
156
+
</Text>
157
+
}
158
+
action={() => {
159
+
setFilter({ ...defaultFilter });
160
+
closePopout();
161
+
}}
162
+
/>
163
+
</MenuGroup>
164
+
</Menu>
165
+
</div>
166
+
);
167
+
}
168
+
169
+
function TagButtonPopout({
170
+
selectedTags,
171
+
setSelectedTags,
172
+
setPopoutRef,
173
+
closePopout
174
+
}: any) {
175
+
return (
176
+
<Dialog ref={setPopoutRef} className={FilterDialogClasses.container}>
177
+
<div className={FilterDialogClasses.header}>
178
+
<div className={FilterDialogClasses.headerLeft}>
179
+
<Heading
180
+
color="interactive-normal"
181
+
variant="text-xs/bold"
182
+
className={FilterDialogClasses.headerText}
183
+
>
184
+
Select tags
185
+
</Heading>
186
+
<div className={FilterDialogClasses.countContainer}>
187
+
<Text
188
+
className={FilterDialogClasses.countText}
189
+
color="none"
190
+
variant="text-xs/medium"
191
+
>
192
+
{selectedTags.size}
193
+
</Text>
194
+
</div>
195
+
</div>
196
+
</div>
197
+
<div className={FilterDialogClasses.tagContainer}>
198
+
{Object.keys(tagNames).map((tag) => (
199
+
<TagItem
200
+
key={tag}
201
+
className={FilterDialogClasses.tag}
202
+
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
203
+
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
204
+
selected={selectedTags.has(tag)}
205
+
/>
206
+
))}
207
+
</div>
208
+
<div className={FilterDialogClasses.separator} />
209
+
<Button
210
+
look={Button.Looks.LINK}
211
+
size={Button.Sizes.MIN}
212
+
color={Button.Colors.CUSTOM}
213
+
className={FilterDialogClasses.clear}
214
+
onClick={() => {
215
+
setSelectedTags(new Set());
216
+
closePopout();
217
+
}}
218
+
>
219
+
<Text variant="text-sm/medium" color="text-link">
220
+
Clear all
221
+
</Text>
222
+
</Button>
223
+
</Dialog>
224
+
);
225
+
}
226
+
227
+
return function FilterBar({
228
+
filter,
229
+
setFilter,
230
+
selectedTags,
231
+
setSelectedTags
232
+
}: {
233
+
filter: Filter;
234
+
setFilter: (filter: Filter) => void;
235
+
selectedTags: Set<string>;
236
+
setSelectedTags: (tags: Set<string>) => void;
237
+
}) {
238
+
const windowSize = Flux.useStateFromStores([WindowStore], () =>
239
+
WindowStore.windowSize()
240
+
);
241
+
242
+
const tagsContainer = React.useRef<HTMLDivElement>(null);
243
+
const tagListInner = React.useRef<HTMLDivElement>(null);
244
+
const [tagsButtonOffset, setTagsButtonOffset] = React.useState(0);
245
+
React.useLayoutEffect(() => {
246
+
if (tagsContainer.current === null || tagListInner.current === null)
247
+
return;
248
+
const { left: containerX, top: containerY } =
249
+
tagsContainer.current.getBoundingClientRect();
250
+
let offset = 0;
251
+
for (const child of tagListInner.current.children) {
252
+
const {
253
+
right: childX,
254
+
top: childY,
255
+
height
256
+
} = child.getBoundingClientRect();
257
+
if (childY - containerY > height) break;
258
+
const newOffset = childX - containerX;
259
+
if (newOffset > offset) {
260
+
offset = newOffset;
261
+
}
262
+
}
263
+
setTagsButtonOffset(offset);
264
+
}, [windowSize]);
265
+
266
+
return (
267
+
<div
268
+
ref={tagsContainer}
269
+
style={{
270
+
paddingTop: "12px"
271
+
}}
272
+
className={`${FilterBarClasses.tagsContainer} ${Margins.marginBottom8}`}
273
+
>
274
+
<Popout
275
+
renderPopout={({ closePopout }: any) => (
276
+
<FilterButtonPopout
277
+
filter={filter}
278
+
setFilter={setFilter}
279
+
closePopout={closePopout}
280
+
/>
281
+
)}
282
+
position="bottom"
283
+
align="left"
284
+
>
285
+
{(props: any, { isShown }: { isShown: boolean }) => (
286
+
<Button
287
+
{...props}
288
+
size={Button.Sizes.MIN}
289
+
color={Button.Colors.CUSTOM}
290
+
className={FilterBarClasses.sortDropdown}
291
+
innerClassName={FilterBarClasses.sortDropdownInner}
292
+
>
293
+
<ArrowsUpDownIcon />
294
+
<Text
295
+
className={FilterBarClasses.sortDropdownText}
296
+
variant="text-sm/medium"
297
+
color="interactive-normal"
298
+
>
299
+
Sort & filter
300
+
</Text>
301
+
{isShown ? (
302
+
<ChevronSmallUpIcon size={20} />
303
+
) : (
304
+
<ChevronSmallDownIcon size={20} />
305
+
)}
306
+
</Button>
307
+
)}
308
+
</Popout>
309
+
<div className={FilterBarClasses.divider} />
310
+
<div className={FilterBarClasses.tagList}>
311
+
<div ref={tagListInner} className={FilterBarClasses.tagListInner}>
312
+
{Object.keys(tagNames).map((tag) => (
313
+
<TagItem
314
+
key={tag}
315
+
className={FilterBarClasses.tag}
316
+
tag={{ name: tagNames[tag as keyof typeof tagNames] }}
317
+
onClick={() => toggleTag(selectedTags, setSelectedTags, tag)}
318
+
selected={selectedTags.has(tag)}
319
+
/>
320
+
))}
321
+
</div>
322
+
</div>
323
+
<Popout
324
+
renderPopout={({ setPopoutRef, closePopout }: any) => (
325
+
<TagButtonPopout
326
+
selectedTags={selectedTags}
327
+
setSelectedTags={setSelectedTags}
328
+
setPopoutRef={setPopoutRef}
329
+
closePopout={closePopout}
330
+
/>
331
+
)}
332
+
position="bottom"
333
+
align="right"
334
+
>
335
+
{(props: any, { isShown }: { isShown: boolean }) => (
336
+
<Button
337
+
{...props}
338
+
size={Button.Sizes.MIN}
339
+
color={Button.Colors.CUSTOM}
340
+
style={{
341
+
left: tagsButtonOffset
342
+
}}
343
+
// TODO: Use Discord's class name utility
344
+
className={`${FilterBarClasses.tagsButton} ${
345
+
selectedTags.size > 0
346
+
? FilterBarClasses.tagsButtonWithCount
347
+
: ""
348
+
}`}
349
+
innerClassName={FilterBarClasses.tagsButtonInner}
350
+
>
351
+
{selectedTags.size > 0 ? (
352
+
<div
353
+
style={{ boxSizing: "content-box" }}
354
+
className={FilterBarClasses.countContainer}
355
+
>
356
+
<Text
357
+
className={FilterBarClasses.countText}
358
+
color="none"
359
+
variant="text-xs/medium"
360
+
>
361
+
{selectedTags.size}
362
+
</Text>
363
+
</div>
364
+
) : (
365
+
<>All</>
366
+
)}
367
+
{isShown ? (
368
+
<ChevronSmallUpIcon size={20} />
369
+
) : (
370
+
<ChevronSmallDownIcon size={20} />
371
+
)}
372
+
</Button>
373
+
)}
374
+
</Popout>
375
+
</div>
376
+
);
377
+
};
378
+
};
+45
-13
packages/core-extensions/src/moonbase/ui/index.tsx
+45
-13
packages/core-extensions/src/moonbase/ui/index.tsx
···
1
-
import WebpackRequire from "@moonlight-mod/types/discord/require";
1
+
import {
2
+
ExtensionLoadSource,
3
+
ExtensionTag,
4
+
WebpackRequireType
5
+
} from "@moonlight-mod/types";
2
6
import card from "./card";
7
+
import filterBar, { defaultFilter } from "./filterBar";
8
+
import { ExtensionState } from "../types";
3
9
4
10
export enum ExtensionPage {
5
11
Info,
···
7
13
Settings
8
14
}
9
15
10
-
export default (require: typeof WebpackRequire) => {
16
+
export default (require: WebpackRequireType) => {
11
17
const React = require("common_react");
12
18
const spacepack = require("spacepack_spacepack").spacepack;
13
19
const Flux = require("common_flux");
···
16
22
require("moonbase_stores") as typeof import("../webpackModules/stores");
17
23
18
24
const ExtensionCard = card(require);
25
+
const FilterBar = React.lazy(() =>
26
+
filterBar(require).then((c) => ({ default: c }))
27
+
);
19
28
20
29
const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports;
21
30
const SearchBar = spacepack.findByCode("Messages.SEARCH", "hideSearchIcon")[0]
···
32
41
);
33
42
34
43
const [query, setQuery] = React.useState("");
44
+
const [filter, setFilter] = React.useState({ ...defaultFilter });
45
+
const [selectedTags, setSelectedTags] = React.useState(new Set<string>());
35
46
36
47
const sorted = Object.values(extensions).sort((a, b) => {
37
48
const aName = a.manifest.meta?.name ?? a.id;
···
39
50
return aName.localeCompare(bName);
40
51
});
41
52
42
-
const filtered = query.trim().length
43
-
? sorted.filter(
44
-
(ext) =>
45
-
ext.manifest.meta?.name?.toLowerCase().includes(query) ||
46
-
ext.manifest.meta?.tagline?.toLowerCase().includes(query) ||
47
-
ext.manifest.meta?.description?.toLowerCase().includes(query)
53
+
const filtered = sorted.filter(
54
+
(ext) =>
55
+
(ext.manifest.meta?.name?.toLowerCase().includes(query) ||
56
+
ext.manifest.meta?.tagline?.toLowerCase().includes(query) ||
57
+
ext.manifest.meta?.description?.toLowerCase().includes(query)) &&
58
+
[...selectedTags.values()].every(
59
+
(tag) => ext.manifest.meta?.tags?.includes(tag as ExtensionTag)
60
+
) &&
61
+
// This seems very bad, sorry
62
+
!(
63
+
(!filter.core && ext.source.type === ExtensionLoadSource.Core) ||
64
+
(!filter.normal && ext.source.type === ExtensionLoadSource.Normal) ||
65
+
(!filter.developer &&
66
+
ext.source.type === ExtensionLoadSource.Developer) ||
67
+
(!filter.enabled &&
68
+
MoonbaseSettingsStore.getExtensionEnabled(ext.id)) ||
69
+
(!filter.disabled &&
70
+
!MoonbaseSettingsStore.getExtensionEnabled(ext.id)) ||
71
+
(!filter.installed && ext.state !== ExtensionState.NotDownloaded) ||
72
+
(!filter.repository && ext.state === ExtensionState.NotDownloaded)
48
73
)
49
-
: sorted;
74
+
);
50
75
51
76
return (
52
77
<>
53
78
<Text
54
-
style={{
55
-
"margin-bottom": "16px"
56
-
}}
79
+
className={Margins.marginBottom20}
57
80
variant="heading-lg/semibold"
58
81
tag="h2"
59
82
>
···
61
84
</Text>
62
85
<SearchBar
63
86
size={SearchBar.Sizes.MEDIUM}
64
-
className={Margins.marginBottom20}
65
87
query={query}
66
88
onChange={(v: string) => setQuery(v.toLowerCase())}
67
89
onClear={() => setQuery("")}
···
73
95
spellCheck: "false"
74
96
}}
75
97
/>
98
+
<React.Suspense
99
+
fallback={<div className={Margins.marginBottom20}></div>}
100
+
>
101
+
<FilterBar
102
+
filter={filter}
103
+
setFilter={setFilter}
104
+
selectedTags={selectedTags}
105
+
setSelectedTags={setSelectedTags}
106
+
/>
107
+
</React.Suspense>
76
108
{filtered.map((ext) => (
77
109
<ExtensionCard id={ext.id} key={ext.id} />
78
110
))}
+19
-18
packages/core-extensions/src/moonbase/ui/info.tsx
+19
-18
packages/core-extensions/src/moonbase/ui/info.tsx
···
13
13
Incompatible = "incompatible"
14
14
}
15
15
16
+
export const tagNames: Record<ExtensionTag, string> = {
17
+
[ExtensionTag.Accessibility]: "Accessibility",
18
+
[ExtensionTag.Appearance]: "Appearance",
19
+
[ExtensionTag.Chat]: "Chat",
20
+
[ExtensionTag.Commands]: "Commands",
21
+
[ExtensionTag.ContextMenu]: "Context Menu",
22
+
[ExtensionTag.DangerZone]: "Danger Zone",
23
+
[ExtensionTag.Development]: "Development",
24
+
[ExtensionTag.Fixes]: "Fixes",
25
+
[ExtensionTag.Fun]: "Fun",
26
+
[ExtensionTag.Markdown]: "Markdown",
27
+
[ExtensionTag.Voice]: "Voice",
28
+
[ExtensionTag.Privacy]: "Privacy",
29
+
[ExtensionTag.Profiles]: "Profiles",
30
+
[ExtensionTag.QualityOfLife]: "Quality of Life",
31
+
[ExtensionTag.Library]: "Library"
32
+
};
33
+
16
34
export default (require: typeof WebpackRequire) => {
17
35
const React = require("common_react");
18
36
const spacepack = require("spacepack_spacepack").spacepack;
···
137
155
{tags != null && (
138
156
<InfoSection title="Tags">
139
157
{tags.map((tag, i) => {
140
-
const names: Record<ExtensionTag, string> = {
141
-
[ExtensionTag.Accessibility]: "Accessibility",
142
-
[ExtensionTag.Appearance]: "Appearance",
143
-
[ExtensionTag.Chat]: "Chat",
144
-
[ExtensionTag.Commands]: "Commands",
145
-
[ExtensionTag.ContextMenu]: "Context Menu",
146
-
[ExtensionTag.DangerZone]: "Danger Zone",
147
-
[ExtensionTag.Development]: "Development",
148
-
[ExtensionTag.Fixes]: "Fixes",
149
-
[ExtensionTag.Fun]: "Fun",
150
-
[ExtensionTag.Markdown]: "Markdown",
151
-
[ExtensionTag.Voice]: "Voice",
152
-
[ExtensionTag.Privacy]: "Privacy",
153
-
[ExtensionTag.Profiles]: "Profiles",
154
-
[ExtensionTag.QualityOfLife]: "Quality of Life",
155
-
[ExtensionTag.Library]: "Library"
156
-
};
157
-
const name = names[tag];
158
+
const name = tagNames[tag];
158
159
159
160
return (
160
161
<Badge
+2
-3
packages/types/src/coreExtensions.ts
+2
-3
packages/types/src/coreExtensions.ts
···
2
2
import { CommonComponents as CommonComponents_ } from "./coreExtensions/components";
3
3
import { Dispatcher } from "flux";
4
4
import React from "react";
5
-
import { WebpackModuleFunc } from "./discord";
6
-
import WebpackRequire from "./discord/require";
5
+
import { WebpackModuleFunc, WebpackRequireType } from "./discord";
7
6
8
7
export type Spacepack = {
9
8
inspect: (module: number | string) => WebpackModuleFunc | null;
10
9
findByCode: (...args: (string | RegExp)[]) => any[];
11
10
findByExports: (...args: string[]) => any[];
12
-
require: typeof WebpackRequire;
11
+
require: WebpackRequireType;
13
12
modules: Record<string, WebpackModuleFunc>;
14
13
cache: Record<string, any>;
15
14
findObjectFromKey: (exports: Record<string, any>, key: string) => any | null;
+7
packages/types/src/coreExtensions/components.ts
+7
packages/types/src/coreExtensions/components.ts
···
298
298
Avatar: Component;
299
299
Scroller: Component;
300
300
Text: ComponentClass<PropsWithChildren<any>>;
301
+
Heading: ComponentClass<PropsWithChildren<any>>;
301
302
LegacyText: Component;
302
303
Flex: Flex;
303
304
Card: ComponentClass<PropsWithChildren<any>>;
305
+
Popout: ComponentClass<PropsWithChildren<any>>;
306
+
Dialog: ComponentClass<PropsWithChildren<any>>;
307
+
Menu: ComponentClass<PropsWithChildren<any>>;
308
+
MenuItem: ComponentClass<PropsWithChildren<any>>;
309
+
MenuGroup: ComponentClass<PropsWithChildren<any>>;
310
+
MenuCheckboxItem: ComponentClass<PropsWithChildren<any>>;
304
311
CardClasses: {
305
312
card: string;
306
313
cardHeader: string;