feat(groups): 216 icones N&B + fermeture auto menu + suggestions auto

This commit is contained in:
2026-09-27 13:03:58 -04:00
parent 856b38bbea
commit dde0747ba3
3 changed files with 262 additions and 2 deletions
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, HostListener, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DatePipe, DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
@@ -7,7 +7,9 @@ import {
GROUP_COLORS,
GROUP_ICONS,
GROUP_ICON_KEYWORDS,
suggestGroupsForChannels,
SubscriptionsService,
type GroupSuggestion,
type SubscriptionGroup,
type SubscriptionItem,
} from '../../../services/subscriptions.service';
@@ -87,6 +89,52 @@ export class SubscriptionsComponent {
// --- Assignation en lot ---
bulkGroupId = signal<string>('');
// --- Suggestions auto de groupes (analyse des chaînes) ---
showSuggestions = signal<boolean>(false);
suggestBusy = signal<boolean>(false);
readonly suggestions = computed<GroupSuggestion[]>(() =>
suggestGroupsForChannels(
this.items().map((item) => ({
subscriptionId: item.subscriptionId,
title: item.channel?.title || '',
handle: item.channel?.handle || null,
groupIds: this.itemGroupIds(item),
})),
this.groups().map((g) => ({ id: g.id, name: g.name })),
)
);
/** Crée le groupe suggéré (ou complète l'existant) et y assigne les chaînes. */
applySuggestion(s: GroupSuggestion): Promise<void> {
if (this.suggestBusy()) return Promise.resolve();
this.suggestBusy.set(true);
const assignAll = (groupId: string): Promise<void> => {
const calls = s.channelIds
.filter((sid) => !(this.members()[String(sid)] || []).includes(groupId))
.map((sid) => new Promise<void>((resolve) => {
const current = this.members()[String(sid)] || [];
this.subs.setSubscriptionGroups(sid, [...current, groupId]).subscribe({ next: () => resolve(), error: () => resolve() });
}));
return Promise.all(calls).then(() => undefined);
};
const done = () => this.suggestBusy.set(false);
if (s.kind === 'complete' && s.existingGroupId) {
return assignAll(s.existingGroupId).finally(done);
}
return new Promise<void>((resolve) => {
this.subs.createGroup(s.name, s.color, s.icon).subscribe((g) => resolve(g?.id ? assignAll(g.id) : undefined));
}).finally(done);
}
/** Applique toutes les suggestions d'un coup, dans l'ordre. */
async applyAllSuggestions(): Promise<void> {
if (this.suggestBusy()) return;
for (const s of this.suggestions()) {
await this.applySuggestion(s);
}
}
readonly items = computed(() => this.subs.items());
readonly groups = computed(() => this.subs.subscriptionGroups());
readonly members = computed(() => this.subs.groupMembers());
@@ -217,6 +265,20 @@ export class SubscriptionsComponent {
this.pickerFor.set(this.pickerFor() === subscriptionId ? null : subscriptionId);
}
/** Ferme le menu dès que le focus/clic sort du menu (clic extérieur, Escape). */
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
if (this.pickerFor() === null) return;
const el = event.target as HTMLElement | null;
if (el?.closest?.('.picker-root')) return;
this.pickerFor.set(null);
}
@HostListener('document:keydown', ['$event'])
onDocumentKey(event: KeyboardEvent): void {
if (event.key === 'Escape' && this.pickerFor() !== null) this.pickerFor.set(null);
}
toggleMembership(item: SubscriptionItem, groupId: string): void {
const current = this.itemGroupIds(item);
const next = current.includes(groupId)