Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/app/analyzer/chart-view/chart-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@
text-transform: capitalize;
}

.chart-tag-remove {
background: none;
border: none;
padding: 0 0 0 3px;
margin: 0;
color: inherit;
opacity: 0.5;
font-size: 0.9em;
cursor: pointer;
}

.chart-tag-remove:hover {
opacity: 1;
}

.chart-tag-editor {
display: block;
margin-top: 4px;
max-width: 260px;
}

.chm-highlight-range {
font-size: 0.9em;
margin: 0;
Expand Down
21 changes: 19 additions & 2 deletions src/app/analyzer/chart-view/chart-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,26 @@ <h3>No Telemetry Loaded</h3>
@if ((file.tags ?? []).length > 0) {
<div class="chart-tags-row">
@for (tag of file.tags; track tag) {
<span class="chart-tag-pill" [style]="tagStyle(tag)">{{ tag }}</span>
<span class="chart-tag-pill" [style]="tagStyle(tag)">
{{ tag }}
<button
class="chart-tag-remove"
type="button"
[title]="'Remove tag ' + tag"
(click)="removeTag(idx, tag)"
>
<i class="fas fa-times"></i>
</button>
</span>
}
</div>
} @if (tagEditorIndex() === idx) {
<app-tag-input
class="chart-tag-editor"
[suggestions]="tagSuggestions()"
(submitted)="submitTag(idx, $event)"
(cancelled)="closeTagEditor()"
/>
}
</div>
<div class="chart-actions chm-flex-center">
Expand Down Expand Up @@ -133,7 +150,7 @@ <h3>No Telemetry Loaded</h3>
>
<i class="fas fa-file-csv"></i>
</button>
<button class="btn-icon" (click)="promptChartTag(idx)" title="Add Tag">
<button class="btn-icon" (click)="openTagEditor(idx)" title="Add Tag">
<i class="fas fa-tags"></i>
</button>
<button class="btn-icon chm-cursor-help" [title]="shortcutsText">
Expand Down
52 changes: 31 additions & 21 deletions src/app/analyzer/chart-view/chart-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import Hammer from 'hammerjs';
import { AppStateService } from '../../core/app-state.service';
import { DataProcessorService } from '../../core/data-processor.service';
import { EventBusService } from '../../core/event-bus.service';
import { DriveService } from '../../core/drive.service';
import { MapService } from '../../core/map.service';
import { tagStyle } from '../../core/tags.util';
import {
ActiveHighlight,
EVENTS,
Expand All @@ -47,6 +49,7 @@ import { themeColor } from '../../core/theme.util';
import { UiStateService } from '../../core/ui-state.service';
import { EmbeddedMap } from '../embedded-map/embedded-map';
import { OverlayMap } from '../overlay-map/overlay-map';
import { TagInput } from '../tag-input/tag-input';

Chart.register(
LineController,
Expand Down Expand Up @@ -168,7 +171,7 @@ Alt + Click : Add / Delete Annotation or Highlight`;
*/
@Component({
selector: 'app-chart-view',
imports: [EmbeddedMap, OverlayMap],
imports: [EmbeddedMap, OverlayMap, TagInput],
templateUrl: './chart-view.html',
styleUrl: './chart-view.css',
})
Expand All @@ -180,13 +183,18 @@ export class ChartView {
private readonly preferences = inject(PreferencesService);
private readonly bus = inject(EventBusService);
private readonly dataProcessor = inject(DataProcessorService);
/** Only for `knownTags` -- the tag editor suggests what's already in use anywhere, including logs only present in the Drive listing. */
private readonly drive = inject(DriveService);

protected readonly canvasRefs =
viewChildren<ElementRef<HTMLCanvasElement>>('canvasEl');

/** Keyed by chart index (fileIdx in stack mode, always 0 in overlay mode). */
protected readonly sliderRanges = signal<Record<number, SliderRange>>({});

/** File index whose inline tag editor is open, or null — replaces the `window.prompt` the `T` shortcut and Shift+Click used to raise. */
protected readonly tagEditorIndex = signal<number | null>(null);

protected readonly chartInfoIndex = signal<number | null>(null);
/** Bundles the index with its file so the template's `@if...as` doesn't treat index 0 as falsy. */
protected readonly chartInfo = computed(() => {
Expand Down Expand Up @@ -527,32 +535,34 @@ export class ChartView {
return name.replace(/\.(jsonl\.json\.gz|json\.gz|json|csv)$/i, '');
}

/** Port of legacy/src/chartmanager.js's `_promptForTag`. */
protected promptChartTag(index: number): void {
const file = this.appState.files()[index];
if (!file) return;
/** Successor to legacy/src/chartmanager.js's `_promptForTag`: opens the inline editor (TagInput) instead of a `prompt`, so the tags already in use can be offered while typing. */
protected openTagEditor(index: number): void {
if (!this.appState.files()[index]) return;
this.tagEditorIndex.set(index);
}

const newTag = prompt(
`Enter a new tag for ${file.name}\n(e.g., Track, Commute, Rain):`
);
if (!newTag || !newTag.trim()) return;
protected closeTagEditor(): void {
this.tagEditorIndex.set(null);
}

const added = this.appState.addFileTag(index, newTag.trim().toLowerCase());
protected submitTag(index: number, tag: string): void {
this.tagEditorIndex.set(null);
const added = this.appState.addFileTag(index, tag);
if (!added) {
this.appState.showAlert('This tag is already applied to this log.');
}
}

/** Port of legacy/src/chartmanager.js's `_getTagStyle` — deterministic hue per tag name. */
protected tagStyle(tag: string): string {
let hash = 0;
for (let i = 0; i < tag.length; i++) {
hash = tag.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
return `background: hsla(${hue}, 70%, 50%, 0.15); color: var(--text-primary); border: 1px solid hsla(${hue}, 70%, 50%, 0.3);`;
protected removeTag(index: number, tag: string): void {
this.appState.removeFileTag(index, tag);
}

protected tagSuggestions(): string[] {
return this.drive.knownTags();
}

protected readonly tagStyle = tagStyle;

/** Port of legacy/src/chartmanager.js's `showChartInfo`. */
protected showChartInfo(index: number): void {
this.chartInfoIndex.set(index);
Expand Down Expand Up @@ -1015,7 +1025,7 @@ export class ChartView {

const onClick = (event: MouseEvent) => {
if (event.shiftKey && !event.altKey && !hasDragged) {
this.promptChartTag(targetFileIdx);
this.openTagEditor(targetFileIdx);
return;
}
this.handleAltClick(fileIdx, mode, event, canvas);
Expand Down Expand Up @@ -1379,10 +1389,10 @@ export class ChartView {
return;
case 't':
case 'T':
// Only in stack mode: exportDataRange/promptChartTag index by file,
// Only in stack mode: exportDataRange/openTagEditor index by file,
// matching the toolbar buttons that expose them, which only exist
// per-file in the stack-mode template.
if (mode !== 'overlay') this.promptChartTag(fileIdx);
if (mode !== 'overlay') this.openTagEditor(fileIdx);
return;
case 'e':
case 'E':
Expand Down
15 changes: 15 additions & 0 deletions src/app/analyzer/drive-panel/drive-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@
filter: brightness(1.2);
}

.remove-tag-btn {
background: none;
border: none;
padding: 0 0 0 4px;
margin: 0;
color: inherit;
opacity: 0.5;
font-size: 0.9em;
cursor: pointer;
}

.remove-tag-btn:hover {
opacity: 1;
}

.existing-tag-pill.active {
filter: brightness(1.4);
box-shadow: 0 0 0 1px currentcolor;
Expand Down
17 changes: 16 additions & 1 deletion src/app/analyzer/drive-panel/drive-panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,17 @@
(click)="filterByTag(tag, $event)"
>
{{ tag }}
<button
class="remove-tag-btn"
type="button"
[title]="'Remove tag ' + tag"
(click)="removeTag(item, tag, $event)"
>
<i class="fas fa-times"></i>
</button>
</span>
}
<span class="add-tag-btn" (click)="addTag(item, $event)">
<span class="add-tag-btn" (click)="openTagEditor(item, $event)">
<i class="fas fa-plus"></i> Tag
</span>
<span
Expand All @@ -191,6 +199,13 @@
<i class="fas fa-link"></i> Get Link
</span>
</div>
@if (tagEditorFileId() === item.file.id) {
<app-tag-input
[suggestions]="drive.knownTags()"
(submitted)="submitTag(item, $event)"
(cancelled)="closeTagEditor()"
/>
}
</div>
</div>
}
Expand Down
37 changes: 23 additions & 14 deletions src/app/analyzer/drive-panel/drive-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Component, inject, signal } from '@angular/core';
import { AccountService } from '../../core/account.service';
import { AuthService } from '../../core/auth.service';
import { DriveFileEntry, DriveService } from '../../core/drive.service';
import { tagStyle } from '../../core/tags.util';
import { TagInput } from '../tag-input/tag-input';

/**
* Cloud Files section of the sidebar. Ports the sign-in/list/load path,
Expand All @@ -10,7 +12,7 @@ import { DriveFileEntry, DriveService } from '../../core/drive.service';
*/
@Component({
selector: 'app-drive-panel',
imports: [],
imports: [TagInput],
templateUrl: './drive-panel.html',
styleUrl: './drive-panel.css',
})
Expand All @@ -22,6 +24,10 @@ export class DrivePanel {
protected readonly showClientIdInput = signal(false);
protected readonly clientIdDraft = signal('');
protected readonly recentExpanded = signal(false);
/** Drive file id whose inline tag editor is open, or null — only one at a time, like the prompt it replaces. */
protected readonly tagEditorFileId = signal<string | null>(null);

protected readonly tagStyle = tagStyle;

protected connect(): void {
void this.drive.connectAndScan();
Expand Down Expand Up @@ -70,20 +76,23 @@ export class DrivePanel {
void this.drive.loadFile(entry.file.name, entry.file.id);
}

protected addTag(entry: DriveFileEntry, event: Event): void {
protected openTagEditor(entry: DriveFileEntry, event: Event): void {
event.stopPropagation();
this.tagEditorFileId.set(entry.file.id);
}

protected closeTagEditor(): void {
this.tagEditorFileId.set(null);
}

protected submitTag(entry: DriveFileEntry, tag: string): void {
this.tagEditorFileId.set(null);
void this.drive.addTag(entry, tag);
}

protected removeTag(entry: DriveFileEntry, tag: string, event: Event): void {
event.stopPropagation();
const tag = window.prompt('Enter a new tag (e.g., Track, Commute, Rain):');
if (tag) void this.drive.addTag(entry, tag);
}

/** Port of legacy/src/drive.js's `_getTagStyle` — deterministic hue per tag name. */
protected tagStyle(tag: string): string {
let hash = 0;
for (let i = 0; i < tag.length; i++) {
hash = tag.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
return `background: hsla(${hue}, 70%, 50%, 0.15); color: var(--text-primary); border: 1px solid hsla(${hue}, 70%, 50%, 0.3);`;
void this.drive.removeTag(entry, tag);
}

protected filterByTag(tag: string, event: Event): void {
Expand Down
52 changes: 52 additions & 0 deletions src/app/analyzer/tag-input/tag-input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.tag-input {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}

.tag-input-field {
flex: 1 1 120px;
min-width: 90px;
background: var(--surface-1);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 12px;
padding: 2px 8px;
font-size: 0.7em;
font-family: inherit;
}

.tag-input-field:focus {
outline: none;
border-color: var(--accent);
}

.tag-input-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 12px;
padding: 2px 7px;
font-size: 0.7em;
line-height: 1.4;
cursor: pointer;
transition:
color 0.2s,
border-color 0.2s;
}

.tag-input-btn:hover {
color: var(--text-primary);
}

.tag-input-btn.confirm:hover {
color: var(--accent);
border-color: var(--accent);
}

.tag-input-preview {
flex-basis: 100%;
color: var(--text-secondary);
font-size: 0.65em;
}
Loading
Loading