Merge branch 'dev'
Some checks failed
Release Browser Extension / Publish (push) Failing after 1m59s

This commit is contained in:
thororen1234 2025-08-14 16:56:46 -04:00
commit a8309bc8da
No known key found for this signature in database
30 changed files with 174 additions and 99 deletions

View file

@ -3,6 +3,7 @@ on:
push:
branches:
- main
env:
FORCE_COLOR: true
GITHUB_TOKEN: ${{ secrets.ETOKEN }}
@ -65,10 +66,15 @@ jobs:
rm release/*.map
- name: Upload Equicord Stable
if: ${{ github.ref_name == 'main' }}
run: |
gh release upload latest --clobber dist/release/*
- name: Create Tag
uses: butlerlogic/action-autotag@1.1.2
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
- name: Upload Plugin JSONs to Equibored repo
run: |
git config --global user.name "GitHub-Actions"

View file

@ -37,9 +37,3 @@ jobs:
- name: Test if plugin structure is valid
run: pnpm generatePluginJson
- name: Create Tag
if: ${{ github.ref_name == 'main' }}
uses: butlerlogic/action-autotag@1.1.2
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.12.10",
"version": "1.12.11",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {
@ -115,4 +115,4 @@
"engines": {
"node": ">=18"
}
}
}

View file

@ -44,12 +44,7 @@ export type FormDivider = ComponentType<{
}>;
export type FormText = ComponentType<PropsWithChildren<{
disabled?: boolean;
selectable?: boolean;
/** defaults to FormText.Types.DEFAULT */
type?: string;
}> & TextProps> & { Types: FormTextTypes; };
export type FormText = ComponentType<TextProps>;
export type Tooltip = ComponentType<{
text: ReactNode | ComponentType;

View file

@ -105,7 +105,7 @@ export default definePlugin({
{
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(subText:\i\(\),)/,
match: /(withDisplayNameStyles\]:\i\}\),children:\i\}\),)/,
replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`},"
},
predicate: () => settings.store.dmList,

View file

@ -23,7 +23,7 @@ function warningEmbedNotice(trigger) {
<Forms.FormText>
Your message contains a term on the automod preset list. (Term "{trigger}")
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
<Forms.FormText>
There is a high chance your message will be blocked and potentially moderated by a server moderator.
</Forms.FormText>
</div>,

View file

@ -86,7 +86,7 @@ function ResetCard() {
<Forms.FormText>
Saving your base profile will allow you to have a backup of your actual profile
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
<Forms.FormText>
If you save, it will overwrite your previous data.
</Forms.FormText>
</div>,
@ -107,7 +107,7 @@ function ResetCard() {
<Forms.FormText>
Loading your base profile will restore your actual profile settings
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
<Forms.FormText>
If you load, it will overwrite your current profile configuration.
</Forms.FormText>
</div>,

View file

@ -95,7 +95,7 @@ function SettingSection() {
return (
<Forms.FormSection>
<Forms.FormTitle>Media source to stream</Forms.FormTitle>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Resets to main screen if not found</Forms.FormText>
<Forms.FormText>Resets to main screen if not found</Forms.FormText>
<ScreenSetting />
</Forms.FormSection>
);

View file

@ -322,8 +322,8 @@ export default definePlugin({
{
find: "location:\"RecentsPopout\"});",
replacement: {
match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?setTab:(\i),onJump:(\i),badgeState:\i,closePopout:(\i)/,
replace: ": $1 === 8 ? $self.tryKeywordMenu($2, $3, $4) $&"
match: /(?<=setTab:(\i),badgeState:\i,closePopout:(\i).{0,50}):(\i)===\i\.\i\.MENTIONS\?\(0,.+?onJump:(\i)/,
replace: ": $3 === 8 ? $self.tryKeywordMenu($1, $2, $4) $&"
}
},
{

View file

@ -10,7 +10,8 @@ import { makeRange, OptionType } from "@utils/types";
import { Button, Forms, MaskedLink, showToast, Toasts } from "@webpack/common";
import hoverOnlyStyle from "./hoverOnly.css?managed";
import { clearLyricsCache } from "./spotify/lyrics/api";
import { clearLyricsCache, removeTranslations } from "./spotify/lyrics/api";
import languages from "./spotify/lyrics/providers/translator/languages";
import { Provider } from "./spotify/lyrics/providers/types";
const sliderOptions = {
@ -61,6 +62,15 @@ export const settings = definePluginSettings({
{ value: Provider.Lrclib, label: "LRCLIB" },
],
},
TranslateTo: {
description: "Translate lyrics to - Changing this will clear existing translations",
type: OptionType.SELECT,
options: languages,
onChange: async () => {
await removeTranslations();
showToast("Translations cleared", Toasts.Type.SUCCESS);
}
},
LyricsConversion: {
description: "Automatically translate or romanize lyrics",
type: OptionType.SELECT,

View file

@ -29,8 +29,7 @@ export const lyricFetchers = {
export const providers = Object.keys(lyricFetchers) as Provider[];
export async function getLyrics(track: Track | null): Promise<LyricsData | null> {
if (!track) return null;
if (!track.id) return null;
if (!track || !track.id) return null;
const cacheKey = track.id;
const cached = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
@ -76,7 +75,7 @@ export async function clearLyricsCache() {
export async function getLyricsCount(): Promise<number> {
const cache = await DataStore.get(LyricsCacheKey) as Record<string, LyricsData | null>;
return Object.keys(cache).length;
return Object.keys(cache ?? {}).length;
}
export async function updateLyrics(trackId: string, newLyrics: SyncedLyric[], provider: Provider) {

View file

@ -14,51 +14,45 @@ import { LyricsContextMenu } from "./ctxMenu";
import { LyricsModal } from "./modal";
import { cl, NoteSvg, useLyrics } from "./util";
const prevCl = cl("prev");
const nextCl = cl("next");
const currentCl = cl("current");
function LyricsDisplay({ scroll = true }: { scroll?: boolean; }) {
const { ShowMusicNoteOnNoLyrics } = settings.use(["ShowMusicNoteOnNoLyrics"]);
const { lyricsInfo, lyricRefs, currLrcIndex } = useLyrics({ scroll });
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null;
const NoteElement = NoteSvg(cl("music-note"));
const makeClassName = (index: number): string => {
if (currLrcIndex === null) return "";
if (currLrcIndex == null) return prevCl;
const diff = index - currLrcIndex;
if (diff === 0) return cl("current");
return cl(diff > 0 ? "next" : "prev");
if (diff === 0) return currentCl;
return diff > 0 ? nextCl : prevCl;
};
if (!lyricsInfo) {
return ShowMusicNoteOnNoLyrics ? (
<div className="vc-spotify-lyrics"
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <LyricsContextMenu />)}
>
<TooltipContainer text="No synced lyrics found">
{NoteElement}
</TooltipContainer>
</div>
) : null;
}
return (
<div
className="vc-spotify-lyrics"
onClick={() => openModal(props => <LyricsModal rootProps={props} />)}
onClick={() => openModal(props => <LyricsModal props={props} />)}
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <LyricsContextMenu />)}
>
{currentLyrics?.map((line, i) => (
{currentLyrics ? currentLyrics.map((line, i) => (
<div ref={lyricRefs[i]} key={i}>
<Text
variant={currLrcIndex === i ? "text-sm/normal" : "text-xs/normal"}
className={makeClassName(i)}
>
{line.text || NoteElement}
{line.text || NoteSvg()}
</Text>
</div>
))}
)) : ShowMusicNoteOnNoLyrics ? (
<TooltipContainer text="No synced lyrics found">
<NoteSvg />
</TooltipContainer>
) : null}
</div>
);
}

View file

@ -17,7 +17,14 @@ const formatTime = (time: number) => {
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
};
function ModalHeaderContent({ track }: { track: Track; }) {
function ModalHeaderContent({ track }: { track: Track | null; }) {
if (!track) {
return (
<ModalHeader>
<Text variant="text-sm/semibold">No track playing</Text>
</ModalHeader>
);
}
return (
<ModalHeader>
<div className={cl("header-content")}>
@ -43,27 +50,30 @@ function ModalHeaderContent({ track }: { track: Track; }) {
);
}
export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) {
const modalCurrentLine = cl("modal-line-current");
const modalLine = cl("modal-line");
export function LyricsModal({ props }: { props: ModalProps; }) {
const { track, lyricsInfo, currLrcIndex } = useLyrics({ scroll: false });
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null;
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric];
return (
<ModalRoot {...rootProps}>
<ModalHeaderContent track={track!} />
<ModalRoot {...props}>
<ModalHeaderContent track={track} />
<ModalContent>
<div className={cl("lyrics-modal-container") + ` ${scrollClasses.auto}`}>
<div className={`${cl("lyrics-modal-container")} ${scrollClasses.auto}`}>
{currentLyrics ? (
currentLyrics.map((line, i) => (
<Text
key={i}
variant={currLrcIndex === i ? "text-md/semibold" : "text-sm/normal"}
selectable
className={currLrcIndex === i ? cl("modal-line-current") : cl("modal-line")}
className={currLrcIndex === i ? modalCurrentLine : modalLine}
>
<span className={cl("modal-timestamp")} onClick={() => SpotifyStore.seek(line.time * 1000)}>
{formatTime(line.time)}
</span>
{line.text || NoteSvg(cl("modal-note"))}
{line.text || NoteSvg()}
</Text>
))
) : (

View file

@ -12,23 +12,51 @@ import { settings } from "../../../settings";
import { SpotifyStore } from "../../SpotifyStore";
import { SpotifyLrcStore } from "../providers/store";
import { SyncedLyric } from "../providers/types";
export const scrollClasses = findByPropsLazy("auto", "customTheme");
export const cl = classNameFactory("vc-spotify-lyrics-");
export function NoteSvg(className: string) {
export function NoteSvg() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 480 720" fill="currentColor" className={className} >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 480 720" fill="currentColor" className={cl("music-note")}>
<path d="m160,-240 q -66,0 -113,-47 -47,-47 -47,-113 0,-66 47,-113 47,-47 113,-47 23,0 42.5,5.5 19.5,5.5 37.5,16.5 v -422 h 240 v 160 H 320 v 400 q 0,66 -47,113 -47,47 -113,47 z" />
</svg>
);
}
const calculateIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => {
const posInSec = position / 1000;
const currentIndex = lyrics.findIndex(l => l.time - (delay / 1000) > posInSec && l.time < posInSec + 8) - 1;
const nextLyric = lyrics.findIndex(l => l.time >= posInSec);
return [currentIndex, nextLyric];
const getIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => {
const posInSec = (position + delay) / 1000;
let left = 0, right = lyrics.length - 1;
let currentIndex: number | null = null;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const curr = lyrics[mid];
const next = lyrics[mid + 1];
if (curr.time <= posInSec && (!next || next.time > posInSec)) {
currentIndex = mid;
break;
}
if (curr.time > posInSec) {
right = mid - 1;
} else {
left = mid + 1;
}
}
const nextIdx = currentIndex !== null ? currentIndex + 1 : left;
const nextLyricIdx = nextIdx < lyrics.length ? nextIdx : null;
if (currentIndex !== null && posInSec - lyrics[currentIndex].time > 8) {
return [null, nextLyricIdx];
}
return [currentIndex, nextLyricIdx];
};
export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) {
@ -47,7 +75,7 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) {
const [position, setPosition] = useState(storePosition);
const [lyricRefs, setLyricRefs] = useState<React.RefObject<HTMLDivElement | null>[]>([]);
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null;
const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric];
useEffect(() => {
if (currentLyrics) {
@ -57,12 +85,15 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) {
useEffect(() => {
if (currentLyrics && position) {
const [currentIndex, nextLyric] = calculateIndexes(currentLyrics, position, LyricDelay);
if (currentLyrics && position != null) {
const [currentIndex, nextLyricIndex] = getIndexes(currentLyrics, position, LyricDelay);
setCurrLrcIndex(currentIndex);
setNextLyric(nextLyric);
setNextLyric(nextLyricIndex);
} else {
setCurrLrcIndex(null);
setNextLyric(null);
}
}, [currentLyrics, position]);
}, [currentLyrics, position, LyricDelay]);
useEffect(() => {
if (scroll && currLrcIndex !== null) {
@ -73,17 +104,15 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) {
lyricRefs[nextLyric]?.current?.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}, [currLrcIndex, nextLyric, scroll]);
}, [currLrcIndex, nextLyric, scroll, lyricRefs]);
useEffect(() => {
if (isPlaying) {
setPosition(SpotifyStore.position);
const interval = setInterval(() => {
setPosition(p => p + 1000);
}, 1000);
if (!isPlaying) return;
return () => clearInterval(interval);
}
setPosition(SpotifyStore.position);
const interval = setInterval(() => setPosition(p => p + 1000), 1000);
return () => clearInterval(interval);
}, [storePosition, isPlaying]);
return { track, lyricsInfo, lyricRefs, currLrcIndex, nextLyric };

View file

@ -5,7 +5,7 @@
*/
import settings from "../../../..";
import { LyricsData, Provider, SyncedLyric } from "../types";
import { Provider, SyncedLyric } from "../types";
// stolen from src\plugins\translate\utils.ts
@ -45,7 +45,7 @@ async function googleTranslate(text: string, targetLang: string, romanize: boole
}
async function processLyrics(
lyrics: LyricsData["lyricsVersions"][Provider],
lyrics: SyncedLyric[],
targetLang: string,
romanize: boolean
): Promise<SyncedLyric[] | null> {
@ -67,7 +67,7 @@ async function processLyrics(
})
);
if (processedLyricsResp[0][1] === null) return null;
if (processedLyricsResp.every(mapping => mapping[1] === null)) return null;
return lyrics.map(lyric => ({
...lyric,
@ -75,13 +75,12 @@ async function processLyrics(
}));
}
async function translateLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise<SyncedLyric[] | null> {
const language = settings.store.TranslateTo;
return processLyrics(lyrics, language, false);
async function translateLyrics(lyrics: SyncedLyric[]) {
return await processLyrics(lyrics, settings.store.TranslateTo, false);
}
async function romanizeLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise<SyncedLyric[] | null> {
return processLyrics(lyrics, "", true);
async function romanizeLyrics(lyrics: SyncedLyric[]) {
return await processLyrics(lyrics, "", true);
}
export const lyricsAlternativeFetchers = {

View file

@ -243,4 +243,4 @@
.eq-tidal-lyrics-word-active {
transform: none;
}
}
}

View file

@ -84,7 +84,7 @@ export default definePlugin({
{
find: "this.renderEmbeddedActivity()",
replacement: {
match: /(?<=render\(\).{0,500}children.{0,50})"div"(?=.{0,500}this\.renderEmbeddedActivity\(\))/,
match: /"div"(?=.{0,100}this.renderActions)/,
replace: "$self.WrapperComponent"
}
}

View file

@ -126,8 +126,8 @@ export default definePlugin({
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
replacement: [
{
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$1$self.wrapMenu($2)"
match: /(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($1)"
},
{
match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,

View file

@ -60,7 +60,7 @@ const AllowLevelSettings = ErrorBoundary.wrap(() => {
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Always allow loggers of these types</Forms.FormText>
<Forms.FormText className={Margins.bottom8}>Always allow loggers of these types</Forms.FormText>
<Flex flexDirection="row">
{Object.keys(settings.store.allowLevel).map(key => (
<AllowLevelSetting key={key} settingKey={key as keyof AllowLevels} />

View file

@ -24,7 +24,7 @@ export const settings = definePluginSettings({
return <div>
<DecorSection hideTitle hideDivider noMargin />
<Forms.FormText type="description" className={classes(Margins.top8, Margins.bottom8)}>
<Forms.FormText className={classes(Margins.top8, Margins.bottom8)}>
You can also access Decor decorations from the <Link
href="/settings/profile-customization"
onClick={e => {

View file

@ -83,7 +83,7 @@ function SectionHeader({ section }: SectionHeaderProps) {
/>}
</Flex>
{hasSubtitle &&
<Forms.FormText type="description" className={Margins.bottom8}>
<Forms.FormText className={Margins.bottom8}>
{section.subtitle}
</Forms.FormText>
}

View file

@ -97,7 +97,7 @@ function CreateDecorationModal(props: ModalProps) {
filters={[{ name: "Decoration file", extensions: ["png", "apng"] }]}
onFileSelect={setFile}
/>
<Forms.FormText type="description" className={Margins.top8}>
<Forms.FormText className={Margins.top8}>
File should be APNG or PNG.
</Forms.FormText>
</Forms.FormSection>
@ -107,7 +107,7 @@ function CreateDecorationModal(props: ModalProps) {
value={name}
onChange={setName}
/>
<Forms.FormText type="description" className={Margins.top8}>
<Forms.FormText className={Margins.top8}>
This name will be used when referring to this decoration.
</Forms.FormText>
</Forms.FormSection>

View file

@ -805,7 +805,7 @@ export default definePlugin({
however you do not have permissions to embed links in the current channel.
Are you sure you want to send this message? Your FakeNitro items will appear as a link only.
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
<Forms.FormText>
You can disable this notice in the plugin settings.
</Forms.FormText>
</div>,

View file

@ -82,7 +82,7 @@ function recalculateActivities() {
function ImportCustomRPCComponent() {
return (
<Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>
<Forms.FormText>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>
<div>
<Button
onClick={() => {
@ -133,7 +133,7 @@ function IdsListComponent(props: { setValue: (value: string) => void; }) {
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
<Forms.FormText className={Margins.bottom8}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
<TextInput
type="text"
value={idsList}

View file

@ -61,6 +61,11 @@ export const settings = definePluginSettings({
type: OptionType.CUSTOM,
default: {} as Record<string, Category[]>,
description: "",
},
disableCreateDMButton: {
type: OptionType.BOOLEAN,
description: "Disabled the create dm button",
default: true
}
});
@ -154,6 +159,14 @@ export default definePlugin({
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
}
},
{
find: ".createDMButtonContainer",
replacement: {
match: /"full-width"===\i/,
replace: "$&&&false"
},
predicate: () => settings.store.disableCreateDMButton
},
],
sections: null as number[] | null,

View file

@ -225,7 +225,7 @@ export default definePlugin({
},
{
// Hide the new version of unreads box for hidden channels
find: '="ChannelListUnreadsStore",',
find: '"ChannelListUnreadsStore",',
replacement: {
match: /(?<=\.id\)\))(?=&&\(0,\i\.\i\)\((\i)\))/,
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`

View file

@ -153,7 +153,7 @@ export default definePlugin({
replace: "false"
},
{
match: /return\s*?\[.{0,50}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/,
match: /return\s*?\[.{0,100}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/,
replace: "return [true"
},
{

View file

@ -0,0 +1,25 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as t from "@vencord/discord-types";
import { Text } from "./components";
// TODO: replace with our own component soon
export const FormText: t.FormText = function FormText(props) {
const variant = props.variant || "text-sm/normal";
return (
<Text
variant={variant}
{...props}
>
{props.children}
</Text>
);
} as any;
// @ts-expect-error
FormText.Types = {};

View file

@ -20,11 +20,11 @@ import { LazyComponent } from "@utils/lazyReact";
import * as t from "@vencord/discord-types";
import { filters, mapMangledModuleLazy, waitFor } from "@webpack";
import { FormText } from "./FormText";
import { waitForComponent } from "./internal";
const FormTitle = waitForComponent<t.FormTitle>("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"'));
const FormText = waitForComponent<t.FormText>("FormText", filters.componentByCode(".SELECTABLE),", ".DISABLED:"));
const FormSection = waitForComponent<t.FormSection>("FormSection", filters.componentByCode(".titleId)"));
const FormDivider = waitForComponent<t.FormDivider>("FormDivider", filters.componentByCode(".divider,", ",style:", '"div"', /\.divider,\i\),style:/));

View file

@ -212,6 +212,7 @@ export const DisplayProfileUtils: t.DisplayProfileUtils = mapMangledModuleLazy(/
export const DateUtils: t.DateUtils = mapMangledModuleLazy("millisecondsInUnit:", {
calendarFormat: filters.byCode("sameElse"),
dateFormat: filters.byCode('":'),
isSameDay: filters.byCode("Math.abs(+"),
// TODO: the +? are for compat with the old version - Remove them once no longer needed
isSameDay: filters.byCode(/Math\.abs\(\+?\i-\+?\i\)/),
diffAsUnits: filters.byCode("days:0", "millisecondsInUnit")
});