259 lines
13 KiB
HTML
259 lines
13 KiB
HTML
<ion-header class="ion-no-border">
|
|
<ion-toolbar class="bg-gradient">
|
|
<ion-buttons slot="start">
|
|
<ion-back-button defaultHref="/home" color="secondary"></ion-back-button>
|
|
</ion-buttons>
|
|
<ion-title class="outfit-font">Mio Canto</ion-title>
|
|
</ion-toolbar>
|
|
</ion-header>
|
|
|
|
<ion-content class="ion-padding bg-gradient"
|
|
[class.high-contrast-mode]="isHighContrast"
|
|
[class.drag-over]="isDraggingOver"
|
|
(dragover)="onDragOver($event)"
|
|
(dragleave)="onDragLeave($event)"
|
|
(drop)="onDrop($event)">
|
|
|
|
<div class="drag-overlay" *ngIf="isDraggingOver">
|
|
<div class="drag-message">
|
|
<ion-icon name="document-text-outline"></ion-icon>
|
|
<p>Rilascia l'immagine o il PDF qui per estrarre il testo</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="propose-container">
|
|
<!-- OCR Progress -->
|
|
<div class="ocr-progress-card" *ngIf="isProcessingOCR">
|
|
<ion-spinner name="crescent"></ion-spinner>
|
|
<div class="progress-info">
|
|
<p>Elaborazione in corso...</p>
|
|
<ion-progress-bar [value]="ocrProgress" color="secondary"></ion-progress-bar>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Segment Control for mobile layout -->
|
|
<ion-segment [(ngModel)]="activeTab" class="custom-mobile-segment ion-margin-bottom" mode="md">
|
|
<ion-segment-button value="editor">
|
|
<ion-label>Editor</ion-label>
|
|
</ion-segment-button>
|
|
<ion-segment-button value="preview">
|
|
<ion-label>Anteprima</ion-label>
|
|
</ion-segment-button>
|
|
</ion-segment>
|
|
|
|
<div class="editor-preview-split">
|
|
<!-- Left Column: Inputs & Editor -->
|
|
<div class="editor-column" [class.hide-on-mobile]="activeTab === 'preview'">
|
|
<ion-list lines="none" class="input-list">
|
|
<ion-item class="custom-input-item">
|
|
<ion-label position="stacked">Titolo del Canto</ion-label>
|
|
<ion-input [(ngModel)]="title" placeholder="Es: Il Signore è la mia salvezza"></ion-input>
|
|
</ion-item>
|
|
|
|
<div class="category-selectors">
|
|
<ion-item class="custom-input-item select-item">
|
|
<ion-label position="stacked">Momento Liturgico</ion-label>
|
|
<ion-select [(ngModel)]="selectedLiturgico" placeholder="Scegli momento" multiple="true" interface="popover">
|
|
<ion-select-option *ngFor="let lit of sortedLiturgico" [value]="lit.id">
|
|
{{ lit.tag_name }}
|
|
</ion-select-option>
|
|
</ion-select>
|
|
</ion-item>
|
|
|
|
<ion-item class="custom-input-item select-item">
|
|
<ion-label position="stacked">Periodo / Tema</ion-label>
|
|
<ion-select [(ngModel)]="selectedTematico" placeholder="Scegli tema" multiple="true" interface="popover">
|
|
<ion-select-option *ngFor="let tem of sortedTematico" [value]="tem.id">
|
|
{{ tem.tag_name }}
|
|
</ion-select-option>
|
|
</ion-select>
|
|
</ion-item>
|
|
</div>
|
|
|
|
<!-- TOOLBARS -->
|
|
<div class="toolbar-section">
|
|
<div class="horizontal-toolbar">
|
|
<ion-button *ngFor="let tag of commonTags" size="small" fill="outline" (click)="insertText(tag.start)">
|
|
{{ tag.label }}
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Chords Selector Toolbar -->
|
|
<div class="toolbar-section">
|
|
<div class="horizontal-toolbar main-chords-toolbar">
|
|
<ion-button
|
|
*ngFor="let group of groupedChords"
|
|
size="small"
|
|
[fill]="selectedRootChord === group.root ? 'solid' : 'outline'"
|
|
[color]="selectedRootChord === group.root ? 'secondary' : 'light'"
|
|
(click)="selectRoot(group.root)">
|
|
{{ group.root }}
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Variations Toolbar (only visible if a root chord is selected) -->
|
|
<div class="toolbar-section variations-container" *ngIf="selectedRootChord">
|
|
<div class="horizontal-toolbar variations-toolbar">
|
|
<span class="variations-label">Variazioni {{ selectedRootChord }}:</span>
|
|
<ion-button
|
|
size="small"
|
|
*ngFor="let chord of getVariations()"
|
|
(click)="insertChord(chord)">
|
|
{{ chord }}
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EDITOR AREA -->
|
|
<div class="editor-wrapper" [class.hc]="isHighContrast">
|
|
<div class="editor-header">
|
|
<div class="editor-title-group" style="display: flex; align-items: center; gap: 8px;">
|
|
<span>Editor Testo</span>
|
|
|
|
<!-- Transpose buttons in left column, above textarea -->
|
|
<div class="editor-transpose-group" style="display: flex; align-items: center; gap: 4px; background: rgba(255, 255, 255, 0.05); padding: 2px 6px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); margin-left: 12px;">
|
|
<ion-button fill="clear" size="small" (click)="transposeDown()" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 24px;">
|
|
<ion-icon slot="icon-only" name="remove-outline" style="font-size: 1.0rem; color: var(--ion-color-secondary);"></ion-icon>
|
|
</ion-button>
|
|
<span class="val outfit-font" style="font-size: 0.85rem; font-weight: 700; color: var(--ion-color-secondary); min-width: 24px; text-align: center;">T:{{ transposeAmount > 0 ? '+' : '' }}{{ transposeAmount }}</span>
|
|
<ion-button fill="clear" size="small" (click)="transposeUp()" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 24px;">
|
|
<ion-icon slot="icon-only" name="add-outline" style="font-size: 1.0rem; color: var(--ion-color-secondary);"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
|
|
<!-- Set default key button -->
|
|
<ion-button *ngIf="transposeAmount !== 0" fill="outline" color="secondary" size="small" (click)="setDefaultTonalita()" style="margin: 0 0 0 8px; font-size: 10px; font-weight: 700; height: 24px; --border-radius: 6px;">
|
|
Imposta tonalità di default
|
|
</ion-button>
|
|
</div>
|
|
<div class="editor-actions">
|
|
<ion-button fill="clear" size="small" (click)="undo()" [disabled]="undoStack.length === 0" title="Annulla ultima modifica">
|
|
<ion-icon name="arrow-undo-outline"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" size="small" (click)="deduceChords()" [disabled]="!content" title="Deduci accordi per le altre strofe">
|
|
<ion-icon name="musical-notes-outline"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" size="small" (click)="takePhoto()" title="Scatta foto">
|
|
<ion-icon name="camera-outline"></ion-icon>
|
|
</ion-button>
|
|
<ion-button fill="clear" size="small" (click)="chooseFile()" title="Allega file o PDF">
|
|
<ion-icon name="document-attach-outline"></ion-icon>
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-textarea-container">
|
|
<!-- Input textarea -->
|
|
<textarea
|
|
#nativeTextarea
|
|
[(ngModel)]="content"
|
|
placeholder="Scrivi o scansiona..."
|
|
(paste)="onPaste($event)"
|
|
spellcheck="false"
|
|
autocapitalize="none"
|
|
autocomplete="off"
|
|
autocorrect="off">
|
|
</textarea>
|
|
</div>
|
|
</div>
|
|
<ion-item class="custom-input-item">
|
|
<ion-label position="stacked">Autore</ion-label>
|
|
<ion-input [(ngModel)]="author" placeholder="Autore"></ion-input>
|
|
</ion-item>
|
|
|
|
<!-- YouTube Link and Player Section -->
|
|
<div class="custom-input-item youtube-section">
|
|
<ion-label position="stacked" style="color: #64ffda; font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;">Link YouTube & Audio Player</ion-label>
|
|
<div class="youtube-input-row" style="display: flex; align-items: center; gap: 8px; margin-top: 6px;">
|
|
<ion-input [(ngModel)]="youtubeLink" placeholder="URL YouTube" style="flex: 1;"></ion-input>
|
|
<ion-button fill="solid" color="secondary" size="small" [disabled]="!youtubeLink" (click)="loadEditorAudio()" style="margin: 0; font-weight: 700;">
|
|
Carica
|
|
</ion-button>
|
|
</div>
|
|
|
|
<!-- Small Player controls when playing/loaded -->
|
|
<div class="editor-mini-player" *ngIf="isAudioLoaded()" style="display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 8px; background: rgba(255, 255, 255, 0.05); border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1);">
|
|
<ion-button fill="clear" color="secondary" (click)="togglePlayPause()" class="play-btn" style="margin: 0; --padding-start: 4px; --padding-end: 4px; height: 36px; width: 36px;">
|
|
<ion-icon slot="icon-only" [name]="youtubePlayerService.isPlaying() ? 'pause-sharp' : 'play-sharp'" style="font-size: 1.6rem;"></ion-icon>
|
|
</ion-button>
|
|
|
|
<span class="player-time outfit-font" style="font-size: 0.8rem; color: var(--ion-color-secondary); opacity: 0.8; min-width: 32px; text-align: right;">{{ formatSeconds(youtubePlayerService.videoProgress()) }}</span>
|
|
<ion-range
|
|
[min]="0"
|
|
[max]="youtubePlayerService.videoDuration()"
|
|
[value]="youtubePlayerService.videoProgress()"
|
|
(ionChange)="onSeek($event)"
|
|
color="secondary"
|
|
style="margin: 0; padding: 0 4px; flex: 1; --bar-height: 4px; --knob-size: 12px;">
|
|
</ion-range>
|
|
<span class="player-time outfit-font" style="font-size: 0.8rem; color: var(--ion-color-secondary); opacity: 0.8; min-width: 32px; text-align: left;">{{ formatSeconds(youtubePlayerService.videoDuration()) }}</span>
|
|
</div>
|
|
</div>
|
|
</ion-list>
|
|
|
|
<div class="action-buttons">
|
|
<ion-button expand="block" (click)="saveToMyCanti()" class="send-btn" [disabled]="!title || !content || isProcessingOCR">
|
|
<ion-icon slot="start" name="save-outline"></ion-icon>
|
|
Salva nei Miei Canti
|
|
</ion-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Active Preview Pane -->
|
|
<div class="preview-column" [class.hide-on-mobile]="activeTab === 'editor'">
|
|
<div class="preview-card" [class.hc]="isHighContrast">
|
|
<div class="preview-header">
|
|
<span class="preview-title">Anteprima Attiva</span>
|
|
|
|
<ion-button fill="clear" size="small" (click)="toggleChordsPreview()" class="preview-toggle-btn" style="margin: 0;">
|
|
<ion-icon slot="start" [name]="showChordsPreview ? 'musical-notes-outline' : 'text-outline'"></ion-icon>
|
|
{{ showChordsPreview ? 'Con Accordi' : 'Solo Testo' }}
|
|
</ion-button>
|
|
</div>
|
|
<div class="preview-body">
|
|
<div class="preview-song-header">
|
|
<h2 class="preview-song-title">{{ title || 'Titolo del Canto' }}</h2>
|
|
<p class="preview-song-author" *ngIf="author">{{ author }}</p>
|
|
</div>
|
|
<div class="preview-lyrics-container">
|
|
<div class="lyrics-view">
|
|
<div *ngFor="let section of parsedSections"
|
|
class="section"
|
|
[class.chorus]="section.type === 'chorus'"
|
|
[class.verse-num]="section.type === 'verse_num'">
|
|
|
|
<div *ngIf="section.type === 'chorus'" class="section-label">Rit.</div>
|
|
<div *ngIf="section.type === 'verse_num' && section.verseNumber" class="section-label verse-num-label">{{ section.verseNumber }}.</div>
|
|
|
|
<div *ngFor="let line of section.lines" class="lyric-line">
|
|
<!-- Chord mode -->
|
|
<ng-container *ngIf="showChordsPreview; else textOnly">
|
|
<span *ngFor="let seg of line.segments; let i = index" class="chord-segment" [class.contiguous-next]="lyricsParser.isContiguousNext(line.segments, i)">
|
|
<span *ngIf="seg.chord" class="chord">{{ seg.chord }}</span>
|
|
<span class="seg-text">{{ seg.text }}</span>
|
|
</span>
|
|
</ng-container>
|
|
<!-- Text only mode -->
|
|
<ng-template #textOnly>
|
|
{{ line.text }}
|
|
</ng-template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="preview-empty" *ngIf="!content">
|
|
Il testo formattato apparirà qui mentre scrivi...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden inputs -->
|
|
<input type="file" #cameraInput (change)="onFileSelected($event, true)" accept="image/*" capture="camera" style="display: none;">
|
|
<input type="file" #fileInput (change)="onFileSelected($event, false)" accept="image/*,application/pdf" style="display: none;">
|
|
</ion-content>
|