More poc work around building out annotations in the epub reader.
This commit is contained in:
parent
fa9508c6b7
commit
7c08a8c301
8 changed files with 445 additions and 31 deletions
|
@ -0,0 +1,25 @@
|
||||||
|
<div class="annotation-card"
|
||||||
|
[style.top.px]="position().top"
|
||||||
|
[style.left.px]="position().left"
|
||||||
|
[attr.data-position]="position().isRight ? 'right' : 'left'"
|
||||||
|
[class.hovered]="isHovered()"
|
||||||
|
(mouseenter)="onMouseEnter()"
|
||||||
|
(mouseleave)="onMouseLeave()">
|
||||||
|
|
||||||
|
<div class="annotation-content">
|
||||||
|
<div class="annotation-text">{{ annotationText() }}</div>
|
||||||
|
<div class="annotation-meta">
|
||||||
|
<small>{{ createdDate() | utcToLocaleDate | date:'short' }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="connection-line"
|
||||||
|
[class.hovered]="isHovered()"
|
||||||
|
[style.left.px]="position().connection.startX"
|
||||||
|
[style.top.px]="position().connection.startY"
|
||||||
|
[style.width.px]="position().connection.distance"
|
||||||
|
[style.transform]="'rotate(' + position().connection.angle + 'deg)'"
|
||||||
|
[style.opacity]="isHovered() ? 1 : 0.3">
|
||||||
|
<div class="connection-dot"></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,50 @@
|
||||||
|
// annotation-card.component.scss
|
||||||
|
.annotation-card {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 300px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.annotation-content {
|
||||||
|
.annotation-text {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-meta {
|
||||||
|
color: #6b7280;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-line {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
transform-origin: 0 50%;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
.connection-dot {
|
||||||
|
position: absolute;
|
||||||
|
right: -3px;
|
||||||
|
top: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hovered {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {UtcToLocaleDatePipe} from "../../../_pipes/utc-to-locale-date.pipe";
|
||||||
|
import {DatePipe} from "@angular/common";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-annotation-card',
|
||||||
|
imports: [
|
||||||
|
UtcToLocaleDatePipe,
|
||||||
|
DatePipe
|
||||||
|
],
|
||||||
|
templateUrl: './annotation-card.component.html',
|
||||||
|
styleUrl: './annotation-card.component.scss'
|
||||||
|
})
|
||||||
|
export class AnnotationCardComponent {
|
||||||
|
position = input.required<any>();
|
||||||
|
annotationText = input<string>('This is test text');
|
||||||
|
createdDate = input<Date>(new Date());
|
||||||
|
isHovered = model<boolean>(false);
|
||||||
|
|
||||||
|
mouseEnter = output<void>();
|
||||||
|
mouseLeave = output<void>();
|
||||||
|
|
||||||
|
onMouseEnter() {
|
||||||
|
this.mouseEnter.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave() {
|
||||||
|
this.mouseLeave.emit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1108,7 +1108,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Load the highlight instance with information from the Annotation
|
// TODO: Load the highlight instance with information from the Annotation
|
||||||
//componentRef.instance.highlightClasses =
|
|
||||||
//componentRef.instance.cdRef.markForCheck();
|
//componentRef.instance.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
<span
|
<!--<span class="epub-highlight">-->
|
||||||
[class]="highlightClasses()"
|
<!-- <i class="fa-solid fa-pen-clip" role="button" (click)="toggleHighlight()"></i>-->
|
||||||
[attr.data-highlight-color]="color()">
|
<!-- <span [class]="highlightClasses()" [attr.data-highlight-color]="color()">-->
|
||||||
<i class="fa-solid fa-pen-clip" role="button" (click)="toggleHighlight()"></i>
|
<!-- <ng-content />-->
|
||||||
<ng-content></ng-content>
|
<!-- </span>-->
|
||||||
|
<!--</span>-->
|
||||||
|
|
||||||
|
<span class="epub-highlight"
|
||||||
|
#highlightSpan
|
||||||
|
(mouseenter)="onMouseEnter()"
|
||||||
|
(mouseleave)="onMouseLeave()">
|
||||||
|
<i class="fa-solid fa-pen-clip" role="button" (click)="toggleHighlight()"></i>
|
||||||
|
<span [class]="highlightClasses()" [attr.data-highlight-color]="color()">
|
||||||
|
<ng-content />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,32 +1,104 @@
|
||||||
.epub-highlight {
|
.epub-highlight {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
|
.epub-highlight-blue {
|
||||||
|
background-color: rgba(59, 130, 246, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.epub-highlight-green {
|
||||||
|
background-color: rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(34, 197, 94, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.epub-highlight-blue {
|
// Global styles for annotation cards (since they're appended to body)
|
||||||
background-color: rgba(59, 130, 246, 0.3);
|
::ng-deep .annotation-card,
|
||||||
border-radius: 3px;
|
.annotation-card {
|
||||||
padding: 1px 2px;
|
position: absolute;
|
||||||
}
|
z-index: 1000;
|
||||||
|
width: 200px;
|
||||||
|
//background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
.epub-highlight-green {
|
&[data-position="left"] {
|
||||||
background-color: rgba(34, 197, 94, 0.3);
|
transform: translateX(0);
|
||||||
border-radius: 3px;
|
}
|
||||||
padding: 1px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.epub-highlight-blue:hover {
|
&[data-position="right"] {
|
||||||
background-color: rgba(59, 130, 246, 0.4);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.epub-highlight-green:hover {
|
.annotation-content {
|
||||||
background-color: rgba(34, 197, 94, 0.4);
|
.annotation-text {
|
||||||
}
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.epub-highlight-blue {
|
.annotation-meta {
|
||||||
border-bottom: 1px solid rgba(59, 130, 246, 0.5);
|
color: #6b7280;
|
||||||
}
|
border-top: 1px solid #f3f4f6;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.epub-highlight-green {
|
.connection-line {
|
||||||
border-bottom: 1px solid rgba(34, 197, 94, 0.5);
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
opacity: 0.3; // Default low opacity
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
// Show line on hover
|
||||||
|
&.hovered {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-direction="left"] {
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-direction="right"] {
|
||||||
|
left: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-direction="left"]::after {
|
||||||
|
right: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-direction="right"]::after {
|
||||||
|
left: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
import {Component, computed, input, model} from '@angular/core';
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
ElementRef,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
signal,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import {Annotation} from "../../_models/annotation";
|
||||||
|
|
||||||
export type HighlightColor = 'blue' | 'green';
|
export type HighlightColor = 'blue' | 'green';
|
||||||
|
|
||||||
|
@ -8,23 +20,217 @@ export type HighlightColor = 'blue' | 'green';
|
||||||
templateUrl: './epub-highlight.component.html',
|
templateUrl: './epub-highlight.component.html',
|
||||||
styleUrl: './epub-highlight.component.scss'
|
styleUrl: './epub-highlight.component.scss'
|
||||||
})
|
})
|
||||||
export class EpubHighlightComponent {
|
export class EpubHighlightComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||||
showHighlight = model<boolean>(false);
|
showHighlight = model<boolean>(false);
|
||||||
color = input<HighlightColor>('blue');
|
color = input<HighlightColor>('blue');
|
||||||
|
annotation = input<Annotation | null>(null);
|
||||||
|
isHovered = signal<boolean>(false);
|
||||||
|
|
||||||
|
@ViewChild('highlightSpan', { static: false }) highlightSpan!: ElementRef;
|
||||||
|
|
||||||
|
private resizeObserver?: ResizeObserver;
|
||||||
|
private annotationCardElement?: HTMLElement;
|
||||||
|
|
||||||
|
showAnnotationCard = computed(() => {
|
||||||
|
const annotation = this.annotation();
|
||||||
|
return this.showHighlight() && true; //annotation && annotation?.noteText.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
highlightClasses = computed(() => {
|
highlightClasses = computed(() => {
|
||||||
const baseClass = 'epub-highlight';
|
const baseClass = 'epub-highlight';
|
||||||
|
|
||||||
if (!this.showHighlight()) {
|
if (!this.showHighlight()) {
|
||||||
return baseClass;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorClass = `epub-highlight-${this.color()}`;
|
const colorClass = `epub-highlight-${this.color()}`;
|
||||||
return `${baseClass} ${colorClass}`;
|
return `${colorClass}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cardPosition = computed(() => {
|
||||||
|
console.log('card position called')
|
||||||
|
if (!this.showHighlight() || !this.highlightSpan) return null;
|
||||||
|
|
||||||
|
const rect = this.highlightSpan.nativeElement.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const cardWidth = 200;
|
||||||
|
const cardHeight = 80; // Approximate card height
|
||||||
|
|
||||||
|
// Check if highlight is on left half (< 50%) or right half (>= 50%) of document
|
||||||
|
const highlightCenterX = rect.left + (rect.width / 2);
|
||||||
|
const isOnLeftHalf = highlightCenterX < (viewportWidth * 0.5);
|
||||||
|
|
||||||
|
const cardLeft = isOnLeftHalf
|
||||||
|
? Math.max(20, rect.left - cardWidth - 20) // Left side with margin consideration
|
||||||
|
: Math.min(viewportWidth - cardWidth - 20, rect.right + 20); // Right side
|
||||||
|
|
||||||
|
const cardTop = rect.top + window.scrollY;
|
||||||
|
|
||||||
|
// Calculate connection points
|
||||||
|
const highlightCenterY = rect.top + window.scrollY + (rect.height / 2);
|
||||||
|
const cardCenterY = cardTop + (cardHeight / 2);
|
||||||
|
|
||||||
|
// Connection points
|
||||||
|
const highlightPoint = {
|
||||||
|
x: isOnLeftHalf ? rect.left : rect.right,
|
||||||
|
y: highlightCenterY
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardPoint = {
|
||||||
|
x: isOnLeftHalf ? cardLeft + cardWidth : cardLeft,
|
||||||
|
y: cardCenterY
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate line properties
|
||||||
|
const deltaX = cardPoint.x - highlightPoint.x;
|
||||||
|
const deltaY = cardPoint.y - highlightPoint.y;
|
||||||
|
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: cardTop,
|
||||||
|
left: cardLeft,
|
||||||
|
isRight: !isOnLeftHalf,
|
||||||
|
connection: {
|
||||||
|
startX: highlightPoint.x,
|
||||||
|
startY: highlightPoint.y,
|
||||||
|
endX: cardPoint.x,
|
||||||
|
endY: cardPoint.y,
|
||||||
|
distance: distance,
|
||||||
|
angle: angle
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Monitor viewport changes for repositioning
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
// Trigger recalculation if card is visible
|
||||||
|
if (this.showAnnotationCard()) {
|
||||||
|
this.updateCardPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked() {
|
||||||
|
if (this.showAnnotationCard() && this.cardPosition()) {
|
||||||
|
this.createOrUpdateAnnotationCard();
|
||||||
|
} else {
|
||||||
|
this.removeAnnotationCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.resizeObserver?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter() {
|
||||||
|
this.isHovered.set(true);
|
||||||
|
if (this.annotation() && this.showAnnotationCard()) {
|
||||||
|
//this.showAnnotationCard.update(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave() {
|
||||||
|
this.isHovered.set(false);
|
||||||
|
//this.showAnnotationCard.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
toggleHighlight() {
|
toggleHighlight() {
|
||||||
this.showHighlight.set(!this.showHighlight());
|
this.showHighlight.set(!this.showHighlight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCardPosition() {
|
||||||
|
// TODO: Figure this out
|
||||||
|
}
|
||||||
|
|
||||||
|
private createOrUpdateAnnotationCard() {
|
||||||
|
const pos = this.cardPosition();
|
||||||
|
if (!pos) return;
|
||||||
|
|
||||||
|
// Remove existing card if it exists
|
||||||
|
this.removeAnnotationCard();
|
||||||
|
|
||||||
|
// Create new card element
|
||||||
|
this.annotationCardElement = document.createElement('div');
|
||||||
|
this.annotationCardElement.className = `annotation-card ${this.isHovered() ? 'hovered' : ''}`;
|
||||||
|
this.annotationCardElement.setAttribute('data-position', pos.isRight ? 'right' : 'left');
|
||||||
|
this.annotationCardElement.style.position = 'absolute';
|
||||||
|
this.annotationCardElement.style.top = `${pos.top}px`;
|
||||||
|
this.annotationCardElement.style.left = `${pos.left}px`;
|
||||||
|
this.annotationCardElement.style.zIndex = '1000';
|
||||||
|
|
||||||
|
// Add event listeners for hover
|
||||||
|
this.annotationCardElement.addEventListener('mouseenter', () => this.onMouseEnter());
|
||||||
|
this.annotationCardElement.addEventListener('mouseleave', () => this.onMouseLeave());
|
||||||
|
|
||||||
|
// Create card content
|
||||||
|
this.annotationCardElement.innerHTML = `
|
||||||
|
<div class="annotation-content">
|
||||||
|
<div class="annotation-text">This is test text</div>
|
||||||
|
<div class="annotation-meta">
|
||||||
|
<small>10/20/2025</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create connection line
|
||||||
|
const lineElement = document.createElement('div');
|
||||||
|
lineElement.className = `connection-line ${this.isHovered() ? 'hovered' : ''}`;
|
||||||
|
lineElement.style.position = 'absolute';
|
||||||
|
lineElement.style.left = `${pos.connection.startX}px`;
|
||||||
|
lineElement.style.top = `${pos.connection.startY}px`;
|
||||||
|
lineElement.style.width = `${pos.connection.distance}px`;
|
||||||
|
lineElement.style.height = '2px';
|
||||||
|
lineElement.style.backgroundColor = '#9ca3af';
|
||||||
|
lineElement.style.transformOrigin = '0 50%';
|
||||||
|
lineElement.style.transform = `rotate(${pos.connection.angle}deg)`;
|
||||||
|
lineElement.style.opacity = this.isHovered() ? '1' : '0.3';
|
||||||
|
lineElement.style.transition = 'opacity 0.2s ease';
|
||||||
|
lineElement.style.zIndex = '999';
|
||||||
|
|
||||||
|
// Add dot at the end
|
||||||
|
const dotElement = document.createElement('div');
|
||||||
|
dotElement.style.position = 'absolute';
|
||||||
|
dotElement.style.right = '-3px';
|
||||||
|
dotElement.style.top = '50%';
|
||||||
|
dotElement.style.width = '6px';
|
||||||
|
dotElement.style.height = '6px';
|
||||||
|
dotElement.style.backgroundColor = '#9ca3af';
|
||||||
|
dotElement.style.borderRadius = '50%';
|
||||||
|
dotElement.style.transform = 'translateY(-50%)';
|
||||||
|
|
||||||
|
lineElement.appendChild(dotElement);
|
||||||
|
|
||||||
|
// Append to body
|
||||||
|
document.body.appendChild(this.annotationCardElement);
|
||||||
|
document.body.appendChild(lineElement);
|
||||||
|
|
||||||
|
// Store reference to line for updates
|
||||||
|
(this.annotationCardElement as any).lineElement = lineElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeAnnotationCard() {
|
||||||
|
if (this.annotationCardElement) {
|
||||||
|
// Remove associated line element
|
||||||
|
const lineElement = (this.annotationCardElement as any).lineElement;
|
||||||
|
if (lineElement) {
|
||||||
|
lineElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.annotationCardElement.remove();
|
||||||
|
this.annotationCardElement = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLineOpacity() {
|
||||||
|
if (this.annotationCardElement) {
|
||||||
|
const lineElement = (this.annotationCardElement as any).lineElement;
|
||||||
|
if (lineElement) {
|
||||||
|
lineElement.style.opacity = this.isHovered() ? '1' : '0.3';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
UI/Web/src/app/book-reader/_models/annotation.ts
Normal file
21
UI/Web/src/app/book-reader/_models/annotation.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export enum HightlightColor {
|
||||||
|
Blue = 1,
|
||||||
|
Green = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Annotation {
|
||||||
|
id: number;
|
||||||
|
xpath: string;
|
||||||
|
endingXPath: string | null;
|
||||||
|
selectedText: string | null;
|
||||||
|
noteText: string;
|
||||||
|
highlightCount: number;
|
||||||
|
hightlightColor: HightlightColor;
|
||||||
|
|
||||||
|
seriesId: number;
|
||||||
|
volumeId: number;
|
||||||
|
chapterId: number;
|
||||||
|
|
||||||
|
createdUtc: string;
|
||||||
|
lastModifiedUtc: string;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue