add varying intensity to stream tags
plus improving the tag select dropdown in general
This commit is contained in:
@@ -1,47 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { favoritedStreams } from '$lib/stores.svelte.ts';
|
||||
import { favoritedStreams, tagList, MAX_TAG_WEIGHT } from '$lib/stores.svelte.ts';
|
||||
import * as streamCache from '$lib/streamCache.svelte.ts';
|
||||
import { getStreamContext } from '$lib/streamContext.svelte.ts';
|
||||
import type { StreamSummary } from '$lib/types';
|
||||
import { hashColor, shorthandCode, formatSecondsToHms, formatDate } from '$lib/utils.ts';
|
||||
import { shorthandCode, formatSecondsToHms, formatDate } from '$lib/utils.ts';
|
||||
import TagSelect from './TagSelect.svelte';
|
||||
import WeightedTags from '$lib/components/WeightedTags.svelte';
|
||||
|
||||
const ctx = getStreamContext();
|
||||
let filteredTags = $state([]);
|
||||
// map of tag -> minimum weight
|
||||
// a stream shows if it has every tag with at least the minimum weight
|
||||
let filteredTags: Record<string, number> = $state({});
|
||||
let favoritesOnly = $state(false);
|
||||
let cachedOnly = $state(false);
|
||||
let listOpen = $state(false);
|
||||
let displayedStreams = $derived.by(streamsToDisplay);
|
||||
let remainingTags = $derived.by(getRemainingTags);
|
||||
|
||||
// how many displayed streams have each unselected tag, at each weight?
|
||||
// used in the tag select dropdown
|
||||
let dropdownCounts = $derived.by(getDropdownCounts);
|
||||
|
||||
// how many displayed streams would be shown
|
||||
// if filtered per each of this tag's weights,
|
||||
// all other filters unchanged?
|
||||
// used for the selected tag pills
|
||||
let pillCounts = $derived.by(getPillCounts);
|
||||
|
||||
// which tags can still alter the displayed streams if selected?
|
||||
// we eliminate either those which aren't present in the displayed streams
|
||||
// or those present (at the same weight) in every displayed stream
|
||||
// decides which tags are visible in the selection dropdown
|
||||
let remainingTags = $derived(
|
||||
Object.keys(dropdownCounts).filter((tag) =>
|
||||
dropdownCounts[tag].some((count) => count > 0 && count < displayedStreams.size)
|
||||
)
|
||||
);
|
||||
|
||||
let cacheUsage = $derived(streamCache.usage.bytes);
|
||||
|
||||
$effect(() => {
|
||||
if (displayedStreams.length == 1) {
|
||||
listOpen = untrack(() => !listOpen);
|
||||
if (displayedStreams.size == 1) {
|
||||
listOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
function matchesFilter(stream: StreamSummary, filters: Record<string, number>) {
|
||||
return Object.entries(filters).every(([tag, min]) => (stream.tags[tag] ?? 0) >= min);
|
||||
}
|
||||
|
||||
function streamsToDisplay() {
|
||||
return ctx.streams.filter(
|
||||
(stream) =>
|
||||
!('tags' in stream) || filteredTags.every((tag) => stream.tags.includes(tag))
|
||||
return new Set(ctx.streams.filter((stream) => matchesFilter(stream, filteredTags)));
|
||||
}
|
||||
|
||||
// how many of these streams have the tag at weight 1+, 2+ and 3+
|
||||
// e.g. dark at weights [1, 3, 3, None] on four streams gives [3, 2, 2]
|
||||
function weightCounts(streams: Iterable<StreamSummary>, tag: string) {
|
||||
const counts = new Array(MAX_TAG_WEIGHT).fill(0);
|
||||
for (const stream of streams) {
|
||||
const weight = stream.tags[tag] ?? 0;
|
||||
for (let i = 0; i < weight; i++) counts[i]++;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function getDropdownCounts() {
|
||||
const unselected = tagList.filter((tag) => !(tag in filteredTags));
|
||||
return Object.fromEntries(
|
||||
unselected.map((tag) => [tag, weightCounts(displayedStreams, tag)])
|
||||
);
|
||||
}
|
||||
|
||||
function getRemainingTags() {
|
||||
let tagCounts = displayedStreams.reduce((tags: Record<string, number>, stream) => {
|
||||
stream.tags.forEach((tag) => (tags[tag] ? (tags[tag] += 1) : (tags[tag] = 1)));
|
||||
return tags;
|
||||
}, {});
|
||||
return Object.entries(tagCounts)
|
||||
.filter((el) => el[1] != displayedStreams.length)
|
||||
.map((el) => el[0]);
|
||||
}
|
||||
function getPillCounts() {
|
||||
return Object.fromEntries(
|
||||
Object.keys(filteredTags).map((tag) => {
|
||||
const otherFilters = { ...filteredTags };
|
||||
delete otherFilters[tag];
|
||||
|
||||
function getDisplayedTagsInList(streamTags: string[], remainingTags: string[]) {
|
||||
return streamTags.filter((tag) => remainingTags.includes(tag));
|
||||
const candidates = ctx.streams.filter((stream) =>
|
||||
matchesFilter(stream, otherFilters)
|
||||
);
|
||||
return [tag, weightCounts(candidates, tag)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function updateFavorites(stream: StreamSummary) {
|
||||
@@ -52,7 +93,13 @@
|
||||
</script>
|
||||
|
||||
<div class="tag-select">
|
||||
<TagSelect bind:listOpen bind:checked={filteredTags} {remainingTags} />
|
||||
<TagSelect
|
||||
bind:listOpen
|
||||
bind:checked={filteredTags}
|
||||
{remainingTags}
|
||||
{dropdownCounts}
|
||||
{pillCounts}
|
||||
/>
|
||||
<button
|
||||
onclick={() => (cachedOnly = !cachedOnly)}
|
||||
class="material-icons filter-download {cachedOnly
|
||||
@@ -72,7 +119,7 @@
|
||||
{@const isPartial = streamCache.partials.has(stream.id)}
|
||||
{@const current = ctx.current?.id === stream.id}
|
||||
<li
|
||||
hidden={!displayedStreams.includes(stream) ||
|
||||
hidden={!displayedStreams.has(stream) ||
|
||||
(favoritesOnly && !favorited) ||
|
||||
(cachedOnly && !(isCached || isPartial))}
|
||||
class="stream-item {current ? 'current-stream' : ''}"
|
||||
@@ -88,11 +135,11 @@
|
||||
|
||||
<span class="stream-item-length">{formatSecondsToHms(stream.length_seconds)}</span>
|
||||
|
||||
<p class="stream-item-tags" hidden={!remainingTags.length}>
|
||||
Tags: <span hidden={!filteredTags.length}>[...] </span>{getDisplayedTagsInList(
|
||||
stream.tags,
|
||||
remainingTags
|
||||
).join(', ')}
|
||||
<p class="stream-item-tags" hidden={!Object.keys(stream.tags).length}>
|
||||
<span class="stream-item-tags-label">Tags:</span>
|
||||
<span class="stream-item-tags-list"
|
||||
><WeightedTags tags={stream.tags} highlighted={filteredTags} /></span
|
||||
>
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -156,11 +203,27 @@
|
||||
}
|
||||
|
||||
.stream-item-tags {
|
||||
display: flex;
|
||||
clear: both;
|
||||
gap: 0.3em;
|
||||
margin: 0;
|
||||
font-family: Tahoma;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.stream-item-tags[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stream-item-tags-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stream-item-tags-list {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stream-item-id {
|
||||
float: left;
|
||||
font-family: monospace;
|
||||
|
||||
Reference in New Issue
Block a user