Skip to content

Commit

Permalink
Merge pull request #9830 from DestinyItemManager/group-loadouts
Browse files Browse the repository at this point in the history
Group loadouts by season when sorting by edit time
  • Loading branch information
bhollis authored Sep 2, 2023
2 parents f922c97 + 53b8b3e commit 0e03e91
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 13 deletions.
1 change: 1 addition & 0 deletions config/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@
"Empty": "The loadout is empty.",
"NoName": "The loadout needs a name."
},
"Season": "Season {{season}}",
"Snapshot": "Save As In-Game Loadout",
"SortByName": "Sort by name",
"SortByEditTime": "Sort by last edited",
Expand Down
9 changes: 9 additions & 0 deletions src/app/loadout/Loadouts.m.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@
.loadoutRow {
padding-bottom: 10px;
}

.seasonHeader {
composes: flexRow from '../dim-ui/common.m.scss';
align-items: center;
gap: 0.25em;
margin: 16px 0 0 0;
border-bottom: 1px solid var(--theme-text);
font-size: 16px;
}
1 change: 1 addition & 0 deletions src/app/loadout/Loadouts.m.scss.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 63 additions & 13 deletions src/app/loadout/Loadouts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { LoadoutSort } from '@destinyitemmanager/dim-api-types';
import { DestinyAccount } from 'app/accounts/destiny-account';
import { apiPermissionGrantedSelector, languageSelector } from 'app/dim-api/selectors';
import { AlertIcon } from 'app/dim-ui/AlertIcon';
import BungieImage from 'app/dim-ui/BungieImage';
import CharacterSelect from 'app/dim-ui/CharacterSelect';
import PageWithMenu from 'app/dim-ui/PageWithMenu';
import ShowPageLoading from 'app/dim-ui/ShowPageLoading';
Expand All @@ -14,10 +15,12 @@ import { getCurrentStore, getStore } from 'app/inventory/stores-helpers';
import { editLoadout } from 'app/loadout-drawer/loadout-events';
import { InGameLoadout, Loadout } from 'app/loadout-drawer/loadout-types';
import { newLoadout, newLoadoutFromEquipped } from 'app/loadout-drawer/loadout-utils';
import { useD2Definitions } from 'app/manifest/selectors';
import { useSetting } from 'app/settings/hooks';
import { AppIcon, addIcon, faCalculator, uploadIcon } from 'app/shell/icons';
import { querySelector, useIsPhonePortrait } from 'app/shell/selectors';
import { usePageTitle } from 'app/utils/hooks';
import { DestinySeasonDefinition } from 'bungie-api-ts/destiny2';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
Expand Down Expand Up @@ -120,6 +123,9 @@ function Loadouts({ account }: { account: DestinyAccount }) {
editLoadout(loadout, selectedStore.id, { isNew: true });
};

// Insert season headers if we're sorting by edit time
const loadoutRows = useAddSeasonHeaders(loadouts, loadoutSort);

const virtualListRef = useRef<VirtualListRef>(null);
const scrollToLoadout = useCallback(
(id: string) => {
Expand Down Expand Up @@ -191,23 +197,42 @@ function Loadouts({ account }: { account: DestinyAccount }) {
{filterPills}
<WindowVirtualList
ref={virtualListRef}
numElements={loadouts.length}
numElements={loadoutRows.length}
itemContainerClassName={styles.loadoutRow}
estimatedSize={270}
getItemKey={(index) => loadouts[index].id}
getItemKey={(index) => {
const loadoutOrSeason = loadoutRows[index];
return 'id' in loadoutOrSeason ? loadoutOrSeason.id : loadoutOrSeason.startDate!;
}}
>
{(index) => {
const loadout = loadouts[index];
return (
<LoadoutRow
loadout={loadout}
store={selectedStore}
saved={savedLoadoutIds.has(loadout.id)}
equippable={loadout !== currentLoadout}
onShare={setSharedLoadout}
onSnapshotInGameLoadout={handleSnapshot}
/>
);
const loadoutOrSeason = loadoutRows[index];
if ('id' in loadoutOrSeason) {
const loadout = loadoutOrSeason;
return (
<LoadoutRow
loadout={loadout}
store={selectedStore}
saved={savedLoadoutIds.has(loadout.id)}
equippable={loadout !== currentLoadout}
onShare={setSharedLoadout}
onSnapshotInGameLoadout={handleSnapshot}
/>
);
} else {
const season = loadoutOrSeason;
return (
<h3 className={styles.seasonHeader}>
{season.displayProperties.hasIcon && (
<BungieImage height={24} width={24} src={season.displayProperties.icon} />
)}{' '}
{season.displayProperties.name} -{' '}
{t('Loadouts.Season', {
season: season.seasonNumber,
})}
</h3>
);
}
}}
</WindowVirtualList>
{loadouts.length === 0 && <p>{t('Loadouts.NoneMatch', { query })}</p>}
Expand Down Expand Up @@ -251,3 +276,28 @@ function Loadouts({ account }: { account: DestinyAccount }) {
</PageWithMenu>
);
}

function useAddSeasonHeaders(loadouts: Loadout[], loadoutSort: LoadoutSort) {
const defs = useD2Definitions()!;
let loadoutRows: (Loadout | DestinySeasonDefinition)[] = loadouts;
if (loadoutSort === LoadoutSort.ByEditTime) {
const seasons = Object.values(defs.Season.getAll()).sort(
(a, b) => b.seasonNumber - a.seasonNumber
);

// TODO: Map.groupBy
const grouped = new Map<DestinySeasonDefinition, Loadout[]>();
for (const loadout of loadouts) {
const season = seasons.find(
(s) =>
new Date(s.startDate ?? Date.now()).getTime() <= (loadout.lastUpdatedAt ?? Date.now())
)!;
const list = grouped.get(season) ?? [];
list.push(loadout);
grouped.set(season, list);
}

loadoutRows = [...grouped.entries()].flatMap(([season, loadouts]) => [season, ...loadouts]);
}
return loadoutRows;
}
1 change: 1 addition & 0 deletions src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@
"NoName": "The loadout needs a name."
},
"SaveLoadout": "Save Loadout",
"Season": "Season {{season}}",
"Share": {
"Copied": "Copied loadout link to clipboard",
"CopyButton": "Copy Link",
Expand Down

0 comments on commit 0e03e91

Please sign in to comment.