Skip to content

Commit 33910e5

Browse files
authored
feat: add filtered rom counts to list views (#85)
ROM count and total size now shows on all rom list views including Favorites, Tag, and System views. When filtering or searching, the counts will update to "Showing X of Y ROMs" with the filtered size" Fixes #82
1 parent 351e912 commit 33910e5

File tree

6 files changed

+199
-80
lines changed

6 files changed

+199
-80
lines changed

src/components/RomStats.vue

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div class="rom-stats">
3+
<template v-if="isFiltered">
4+
Showing {{ formattedCount }} of {{ formattedTotal }} {{ label }}
5+
<span v-if="size" class="rom-stats__size">({{ formattedSize }})</span>
6+
</template>
7+
<template v-else>
8+
{{ formattedTotal }} {{ label }}
9+
<span v-if="size" class="rom-stats__size">({{ formattedSize }})</span>
10+
</template>
11+
</div>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { computed } from 'vue';
16+
import { formatBytes, formatNumber } from '@/utils/number.utils';
17+
18+
const props = withDefaults(
19+
defineProps<{
20+
total: number;
21+
filtered?: number;
22+
size?: number;
23+
label?: string;
24+
}>(),
25+
{
26+
label: 'ROMs',
27+
}
28+
);
29+
30+
const isFiltered = computed(() => props.filtered !== undefined && props.filtered !== props.total);
31+
32+
const formattedCount = computed(() => formatNumber(props.filtered ?? 0));
33+
const formattedTotal = computed(() => formatNumber(props.total));
34+
const formattedSize = computed(() => formatBytes(props.size ?? 0));
35+
</script>
36+
37+
<style lang="less" scoped>
38+
.rom-stats {
39+
font-size: var(--font-size-sm);
40+
padding: var(--space-4) var(--space-12);
41+
padding-top: var(--space-2);
42+
43+
&__size {
44+
color: var(--p-text-muted-color);
45+
}
46+
}
47+
</style>

src/layouts/RomListLayout.vue

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@
8282
</div>
8383
<div class="rom-list-layout__content">
8484
<div class="rom-list-layout__content-list">
85-
<slot :filtered-roms="filteredRoms" :loading="romStore.loading"></slot>
85+
<slot
86+
:filtered-roms="filteredRoms.roms"
87+
:filtered-size="filteredRoms.size"
88+
:total-roms="sortedRoms.length"
89+
:loading="romStore.loading"
90+
></slot>
8691
</div>
8792
<div class="rom-list-layout__content-detail">
8893
<slot name="rom-details"></slot>
@@ -156,44 +161,60 @@ const filterRegions = computed(() => {
156161
.sort((a, b) => a.label.localeCompare(b.label));
157162
});
158163
164+
const romsForMode = computed(() => {
165+
if (props.mode === 'favorites') {
166+
return romStore.roms.filter((rom) => rom.favorite);
167+
}
168+
169+
if (props.mode === 'system' && props.system) {
170+
return romStore.roms.filter((rom) => rom.system === props.system);
171+
}
172+
173+
if (props.mode === 'tag') {
174+
const tag = props.tag;
175+
if (!tag) return romStore.roms;
176+
return romStore.roms.filter((rom) => (rom.tags || []).includes(tag));
177+
}
178+
179+
return romStore.roms;
180+
});
181+
182+
// TODO: Consider doing sort in the database query instead.
159183
const sortedRoms = computed(() => {
160-
return [...romStore.roms].sort((a, b) => a.displayName.localeCompare(b.displayName));
184+
return [...romsForMode.value].sort((a, b) => a.displayName.localeCompare(b.displayName));
161185
});
162186
163187
const filteredRoms = computed(() => {
164188
const query = searchQuery.value.toLowerCase().trim();
165189
const selectedRegions = filterByRegion.value;
166-
const selectedSystems = props.mode === 'system' ? [props.system] : filterBySystem.value;
167-
const filterTag = props.mode === 'tag' ? props.tag : null;
190+
const selectedSystems = filterBySystem.value;
191+
let size = 0;
168192
169-
return sortedRoms.value.filter((rom) => {
170-
const { displayName, system, region, tags = [] } = rom;
193+
const roms = sortedRoms.value.filter((rom) => {
194+
const { displayName, system, region } = rom;
171195
172196
const hasSystemMatch =
173197
!selectedSystems?.length || selectedSystems.some((value) => value === system);
174198
const hasRegionMatch =
175199
!selectedRegions?.length || selectedRegions.some((value) => value === region);
176200
const hasQueryMatch = !query || displayName.toLowerCase().includes(query);
177-
178-
const hasTagMatch = !filterTag || (tags && tags.includes(filterTag));
179-
const hasFavoritesMatch = props.mode === 'favorites' ? rom.favorite : true;
180201
const hasRAMatch =
181202
!filterByRA.value ||
182203
(filterByRA.value === 'has-achievements' && (rom.numAchievements ?? 0) > 0) ||
183204
(filterByRA.value === 'no-achievements' &&
184205
rom.verified &&
185206
(rom.numAchievements ?? 0) === 0) ||
186207
(filterByRA.value === 'unverified' && !rom.verified);
208+
const shouldInclude = hasSystemMatch && hasRegionMatch && hasQueryMatch && hasRAMatch;
187209
188-
return (
189-
hasSystemMatch &&
190-
hasRegionMatch &&
191-
hasQueryMatch &&
192-
hasTagMatch &&
193-
hasRAMatch &&
194-
hasFavoritesMatch
195-
);
210+
if (shouldInclude) {
211+
size += rom.size ?? 0;
212+
}
213+
214+
return shouldInclude;
196215
});
216+
217+
return { roms, size };
197218
});
198219
199220
function toggleFilters() {

src/views/FavoritesView.vue

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
<template>
2-
<RomListLayout mode="favorites">
3-
<template #default="{ filteredRoms, loading }">
4-
<RomList
5-
class="library-view__content-list"
6-
:loading="loading"
7-
:roms="filteredRoms"
8-
:rom-selections="romSelections"
9-
:compact="false"
10-
@rom-selected="romSelections = $event"
11-
/>
2+
<RomListLayout class="favorites-view" mode="favorites">
3+
<template #default="{ filteredRoms, totalRoms, filteredSize, loading }">
4+
<div class="favorites-view__content">
5+
<RomStats
6+
:filtered="filteredRoms.length"
7+
:total="totalRoms"
8+
:size="filteredSize"
9+
label="favorites"
10+
/>
11+
<RomList
12+
class="favorites-view__list"
13+
:loading="loading"
14+
:roms="filteredRoms"
15+
:rom-selections="romSelections"
16+
:compact="false"
17+
@rom-selected="romSelections = $event"
18+
/>
19+
</div>
1220
</template>
1321
<template #rom-details>
1422
<RomDetailView
@@ -32,6 +40,7 @@ import RomListLayout from '@/layouts/RomListLayout.vue';
3240
import RomDetailView from '@/views/RomDetailView.vue';
3341
import RomActionView from '@/views/RomActionView.vue';
3442
import RomList from '@/components/RomList.vue';
43+
import RomStats from '@/components/RomStats.vue';
3544
3645
const romSelections = ref<string[]>([]);
3746
@@ -42,4 +51,18 @@ function handleFavorite(favorite: boolean) {
4251
}
4352
</script>
4453

45-
<style scoped lang="less"></style>
54+
<style lang="less" scoped>
55+
.favorites-view {
56+
&__content {
57+
height: 100%;
58+
display: flex;
59+
flex-direction: column;
60+
min-height: 0;
61+
}
62+
63+
&__list {
64+
flex: 1;
65+
min-height: 0;
66+
}
67+
}
68+
</style>

src/views/LibraryView.vue

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
<template>
22
<RomListLayout class="library-view" mode="all">
3-
<template #default="{ filteredRoms, loading }">
3+
<template #default="{ filteredRoms, totalRoms, filteredSize, loading }">
44
<div class="library-view__content">
5-
<div class="library-view__summary">
6-
{{ librarySummary }}
7-
</div>
5+
<RomStats :filtered="filteredRoms.length" :total="totalRoms" :size="filteredSize" />
86
<RomList
97
class="library-view__list"
108
:loading="loading"
@@ -31,25 +29,15 @@
3129
</template>
3230

3331
<script setup lang="ts">
34-
import { ref, computed } from 'vue';
35-
import { useRomStore } from '@/stores';
32+
import { ref } from 'vue';
33+
3634
import RomListLayout from '@/layouts/RomListLayout.vue';
3735
import RomDetailView from '@/views/RomDetailView.vue';
3836
import RomActionView from '@/views/RomActionView.vue';
3937
import RomList from '@/components/RomList.vue';
40-
import { formatBytes, formatNumber } from '@/utils/number.utils';
41-
import { pluralize } from '@/utils/string.utils';
38+
import RomStats from '@/components/RomStats.vue';
4239
43-
const romStore = useRomStore();
4440
const romSelections = ref<string[]>([]);
45-
46-
// TODO: Empty state: “Your ROM library is empty. Time to add some games!”
47-
const librarySummary = computed(() => {
48-
const { totalSizeBytes, totalRoms, systemCounts } = romStore.stats;
49-
const totalSystems = Object.keys(systemCounts).length;
50-
51-
return `You have a ${formatBytes(totalSizeBytes)} ROM library with ${formatNumber(totalRoms)} ${pluralize(totalRoms, 'ROM')} across ${formatNumber(totalSystems)} ${pluralize(totalSystems, 'system')}.`;
52-
});
5341
</script>
5442

5543
<style lang="less" scoped>
@@ -61,13 +49,6 @@ const librarySummary = computed(() => {
6149
min-height: 0; // allow the list to shrink inside flex parent
6250
}
6351
64-
&__summary {
65-
font-size: var(--font-size-sm);
66-
color: var(--p-text-muted-color);
67-
padding: var(--space-4) var(--space-10);
68-
padding-top: var(--space-2);
69-
}
70-
7152
&__list {
7253
flex: 1;
7354
min-height: 0;

src/views/SystemView.vue

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
<template>
2-
<RomListLayout :system="system" mode="system">
3-
<template #default="{ filteredRoms, loading }">
4-
<RomList
5-
class="system-view__content-list"
6-
:loading="loading"
7-
:roms="filteredRoms"
8-
:rom-selections="romSelections"
9-
:compact="false"
10-
@rom-selected="romSelections = $event"
11-
/>
2+
<RomListLayout class="system-view" :system="system" mode="system">
3+
<template #default="{ filteredRoms, totalRoms, filteredSize, loading }">
4+
<div class="system-view__content">
5+
<RomStats
6+
:filtered="filteredRoms.length"
7+
:total="totalRoms"
8+
:size="filteredSize"
9+
:label="statsLabel"
10+
/>
11+
<RomList
12+
class="system-view__list"
13+
:loading="loading"
14+
:roms="filteredRoms"
15+
:rom-selections="romSelections"
16+
:compact="false"
17+
@rom-selected="romSelections = $event"
18+
/>
19+
</div>
1220
</template>
1321
<template #rom-details>
1422
<RomDetailView
@@ -26,16 +34,33 @@
2634
</template>
2735

2836
<script setup lang="ts">
29-
import { ref } from 'vue';
37+
import { computed, ref } from 'vue';
3038
import RomListLayout from '@/layouts/RomListLayout.vue';
3139
import RomDetailView from '@/views/RomDetailView.vue';
3240
import RomActionView from '@/views/RomActionView.vue';
3341
import RomList from '@/components/RomList.vue';
42+
import RomStats from '@/components/RomStats.vue';
43+
import { getSystemDisplayName } from '@/utils/systems';
3444
3545
import type { SystemCode } from '@/types/system';
3646
37-
defineProps<{ system: SystemCode }>();
47+
const props = defineProps<{ system: SystemCode }>();
3848
const romSelections = ref<string[]>([]);
49+
const statsLabel = computed(() => `${getSystemDisplayName(props.system)} ROMs`);
3950
</script>
4051

41-
<style lang="less" scoped></style>
52+
<style lang="less" scoped>
53+
.system-view {
54+
&__content {
55+
height: 100%;
56+
display: flex;
57+
flex-direction: column;
58+
min-height: 0;
59+
}
60+
61+
&__list {
62+
flex: 1;
63+
min-height: 0;
64+
}
65+
}
66+
</style>

src/views/TagView.vue

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
<template>
2-
<RomListLayout :tag="tag" mode="tag">
3-
<template #default="{ filteredRoms, loading }">
4-
<RomList
5-
class="library-view__content-list"
6-
:loading="loading"
7-
:roms="filteredRoms"
8-
:rom-selections="romSelections"
9-
:compact="false"
10-
@rom-selected="romSelections = $event"
11-
/>
2+
<RomListLayout class="tag-view" :tag="tag" mode="tag">
3+
<template #default="{ filteredRoms, totalRoms, filteredSize, loading }">
4+
<div class="tag-view__content">
5+
<RomStats
6+
:filtered="filteredRoms.length"
7+
:total="totalRoms"
8+
:size="filteredSize"
9+
:label="statsLabel"
10+
/>
11+
<RomList
12+
class="tag-view__list"
13+
:loading="loading"
14+
:roms="filteredRoms"
15+
:rom-selections="romSelections"
16+
:compact="false"
17+
@rom-selected="romSelections = $event"
18+
/>
19+
</div>
1220
</template>
1321
<template #rom-details>
1422
<RomDetailView
@@ -26,16 +34,30 @@
2634
</template>
2735

2836
<script setup lang="ts">
29-
import { ref } from 'vue';
30-
import { useRouter } from 'vue-router';
37+
import { ref, computed } from 'vue';
3138
import RomListLayout from '@/layouts/RomListLayout.vue';
3239
import RomDetailView from '@/views/RomDetailView.vue';
3340
import RomActionView from '@/views/RomActionView.vue';
3441
import RomList from '@/components/RomList.vue';
42+
import RomStats from '@/components/RomStats.vue';
3543
36-
defineProps<{ tag: string }>();
37-
useRouter();
44+
const props = defineProps<{ tag: string }>();
3845
const romSelections = ref<string[]>([]);
46+
const statsLabel = computed(() => `ROMs tagged "${props.tag}"`);
3947
</script>
4048

41-
<style scoped lang="less"></style>
49+
<style lang="less" scoped>
50+
.tag-view {
51+
&__content {
52+
height: 100%;
53+
display: flex;
54+
flex-direction: column;
55+
min-height: 0;
56+
}
57+
58+
&__list {
59+
flex: 1;
60+
min-height: 0;
61+
}
62+
}
63+
</style>

0 commit comments

Comments
 (0)