feat(groups): ratissage élargi (3 lettres, sous-chaînes, singletons, plafond, copie inclassables)

This commit is contained in:
2026-09-27 14:05:08 -04:00
parent 63a2bddf47
commit a2b4139887
3 changed files with 103 additions and 24 deletions
@@ -41,7 +41,10 @@
<div class="flex flex-wrap items-center justify-between gap-2">
<div>
<h3 class="font-semibold text-slate-100">✨ Groupes suggérés</h3>
<p class="text-xs text-slate-400">Noms de chaînes + sujets détectés. Vérifiez avant d’appliquer.</p>
<p class="text-xs text-slate-400">
Noms de chaînes + sujets détectés. Vérifiez avant d’appliquer.
<span *ngIf="ungroupedCount() > 0" class="text-amber-200/90">{{ ungroupedCount() }} sans groupe.</span>
</p>
</div>
<div class="flex flex-wrap gap-2">
<button
@@ -79,7 +82,15 @@
</div>
</div>
<div *ngIf="suggestions().length === 0" class="mt-3 rounded-lg border border-dashed border-slate-700 p-4 text-center text-sm text-slate-400">
Aucune suggestion : chaque thème a moins de 2 chaînes, ou tout est déjà rangé.
<p>Aucune suggestion : chaque thème a moins de 2 chaînes, ou tout est déjà rangé.</p>
<button
*ngIf="ungroupedCount() > 0"
type="button"
(click)="copyUngrouped()"
class="mt-2 rounded-md border border-slate-600 px-3 py-1.5 text-xs text-slate-200 transition hover:border-slate-400 hover:text-white"
>
{{ copiedUngrouped() ? 'Copié ! Collez la liste pour enrichir les règles.' : 'Copier les ' + ungroupedCount() + ' sans-groupe' }}
</button>
</div>
<ul *ngIf="suggestions().length > 0" class="mt-3 space-y-2">
<li *ngFor="let s of suggestions(); trackBy: trackBySuggestion" class="flex flex-col gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-3 sm:flex-row sm:items-center sm:justify-between">
@@ -219,6 +219,45 @@ export class SubscriptionsComponent {
}
}
copiedUngrouped = signal<boolean>(false);
/** Copie les chaînes sans groupe (une par ligne) pour analyse des règles. */
copyUngrouped(): void {
const lines = this.items()
.filter((item) => this.itemGroupIds(item).length === 0)
.map((item) => item.channel?.title || item.channel?.handle || `#${item.subscriptionId}`);
const text = lines.join('\n');
if (!text) return;
const done = () => {
this.copiedUngrouped.set(true);
setTimeout(() => this.copiedUngrouped.set(false), 2000);
};
try {
const nav = navigator as Navigator & { clipboard?: { writeText(t: string): Promise<void> } };
if (nav?.clipboard?.writeText) {
nav.clipboard.writeText(text).then(done, () => this.copyUngroupedFallback(text, done));
} else {
this.copyUngroupedFallback(text, done);
}
} catch {
this.copyUngroupedFallback(text, done);
}
}
private copyUngroupedFallback(text: string, done: () => void): void {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
done();
} catch {}
}
// --- trackBy : DOM stable (pas de re-rendu complet + recharge d'images à chaque tick) ---
trackBySubscriptionId(_idx: number, item: SubscriptionItem): number {
return item.subscriptionId;