fix: cannot login with default password
This commit is contained in:
+277
@@ -0,0 +1,277 @@
|
|||||||
|
# Technical Specification: AI Loop Scanning System & Fade-Free Zero-Crossing Slicing
|
||||||
|
|
||||||
|
This document specifies the software architecture, digital signal processing (DSP) algorithms, and API design required to integrate AI-driven automated loop scanning and perfect, fade-free audio slicing (Zero-Crossing Aligned Slicing) without boundary transition effects (Fade-In/Fade-Out).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Feature 1: AI Loop Scan & Automated Marker Labeling
|
||||||
|
|
||||||
|
This feature allows users to quickly scan an audio track (driven by backend AI/DSP) to detect segments with the highest rhythmic or musical periodicity (e.g., drum loops, chord progressions, vocal loops) and automatically map both boundaries using the timeline marker system.
|
||||||
|
|
||||||
|
```text
|
||||||
|
AI LOOP SCAN PROCESSING FLOW
|
||||||
|
┌───────────────────┐ 1. Send File ID ┌────────────────────────┐
|
||||||
|
│ Frontend Client ├──────────────────────►│ Backend FastAPI Server │
|
||||||
|
│ (Click "AI Scan") │◄──────────────────────┤ (Celery Task Worker) │
|
||||||
|
└───────────────────┘ 4. Return timestamps└───────────┬────────────┘
|
||||||
|
▲ [t_start, t_end] │
|
||||||
|
│ ▼
|
||||||
|
│ 2. Analyze Audio Features
|
||||||
|
│ (Self-Similarity Matrix)
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
└───────── (Pin Markers automatically) ◄ 3. Snap to Zero-Crossing
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.1. Workflow
|
||||||
|
|
||||||
|
1. The user selects an audio track within the Main Session and clicks the *AI Scan* button on the AI Panel.
|
||||||
|
2. The frontend dispatches a request containing the track's `file_id` to the backend gateway.
|
||||||
|
3. The backend initiates an asynchronous Celery Task, leveraging the `librosa` acoustic processing library to extract spectral feature matrices (Chromagram/Mel-spectrogram) and search for target loop boundaries exhibiting the highest recurrence correlation.
|
||||||
|
4. Once the optimal loop region $[t_{\text{start}}, t_{\text{end}}]$ is calculated, the backend executes a Zero-Crossing Alignment routine to precisely shift both boundaries to the nearest index where the signal amplitude reaches exactly zero.
|
||||||
|
5. The processed absolute timestamps $[t'_{\text{start}}, t'_{\text{end}}]$ are returned to the client. The frontend dynamically instantiates and renders timeline markers pinned directly onto that track lane.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Feature 2: AI Analysis & AI Cut (Fade-Free)
|
||||||
|
|
||||||
|
When cutting an audio segment at arbitrary time markers, if a slice intersects a high-amplitude point (non-zero), the continuous physical phase of the waveform is abruptly broken (Jump discontinuity). This generates a sharp, vertical step in the amplitude waveform graph, translating mechanically into an audible, harsh popping or ticking artifact ("click" or "pop") through speakers.
|
||||||
|
|
||||||
|
Standard or basic DAW systems mitigate this issue by adding an ultra-short linear fade envelope (Fade-In/Fade-Out) spanning roughly $5\text{ ms} \rightarrow 10\text{ ms}$. However, this masking method dampens the physical attack phase (transients) of the sound field, which is severely destructive to sharp, high-impact hits such as kick drums or snares.
|
||||||
|
|
||||||
|
The perfect architecture is a **Fade-Free AI Cut**. It dynamically calculates the closest hardware zero-crossing indices—where the acoustic wave amplitude passes through the central horizontal timeline axis ($0\text{V}$ absolute silence)—and executes the audio slice precisely at those coordinates.
|
||||||
|
|
||||||
|
```text
|
||||||
|
WAVEFORM TIMELINE & FADE-FREE AI CUT PROCESS
|
||||||
|
Amplitude
|
||||||
|
▲
|
||||||
|
+1.0 ┼ / \ / \
|
||||||
|
│ / \ / \
|
||||||
|
│ User-defined/ \ / \ User-defined
|
||||||
|
│ selection marker \ / \ selection marker
|
||||||
|
0.0 ┼───────○─────────────○─────○─────────○───────► Time Axis
|
||||||
|
│ / \ / \ / \ / \
|
||||||
|
│ / \ / \ / \ / \
|
||||||
|
-1.0 ┼────/──────\──────/─────○─────\───/─────\─
|
||||||
|
▲
|
||||||
|
│ [ AI CUTS EXACTLY HERE ]
|
||||||
|
│ Amplitude = 0 (Sound is silent)
|
||||||
|
│ Absolute zero Click/Pop anomalies!
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1. Workflow
|
||||||
|
|
||||||
|
1. The user left-clicks and drags a time selection window $[T_{\text{start}}, T_{\text{end}}]$ across the target Waveform Lane.
|
||||||
|
2. The user clicks **AI Analysis**: The backend calculates and shifts both bounding coordinates slightly to align with physical zero-crossing sample indices ($T'_{\text{start}}$ and $T'_{\text{end}}$), instantly refreshing the highlighted overlay on the screen viewport.
|
||||||
|
3. The user clicks **AI Cut**: The engine slices the raw binary sample stream from index $T'_{\text{start}}$ to $T'_{\text{end}}$ straight inside RAM, generates a new track row directly underneath, and drops the cut clip onto it. The asset remains un-rendered and pure, with absolutely no volume fade multi-stage nodes applied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Mathematical Zero-Crossing Optimization Algorithm (DSP Math)
|
||||||
|
|
||||||
|
Let $x[n]$ represent a single-channel discrete sample array containing mono audio amplitudes ($0$ mapping to the left track lane channel). At the target sample index address $n_{\text{target}}$ derived from the user's raw timeline click event, the engine establishes a symmetrical boundary scanning window of size $W$ (typically set to a $50\text{ ms}$ horizontal time width):
|
||||||
|
|
||||||
|
$$n_{\text{start}} = n_{\text{target}} - \frac{W \cdot f_s}{2}, \quad n_{\text{end}} = n_{\text{target}} + \frac{W \cdot f_s}{2}$$
|
||||||
|
|
||||||
|
Where $f_s$ tracks the absolute project Sample Rate hardware clock (e.g., $44100\text{ Hz}$).
|
||||||
|
|
||||||
|
### 3.1. Physical Phase Inversion Condition (Zero-Crossing Condition)
|
||||||
|
|
||||||
|
The optimization loop evaluates all internal sample index integers $i \in [n_{\text{start}}, n_{\text{end}}]$ that satisfy the algebraic sign-inversion condition rule:
|
||||||
|
|
||||||
|
$$x[i] \cdot x[i+1] \le 0$$
|
||||||
|
|
||||||
|
### 3.2. Optimization Criterion
|
||||||
|
|
||||||
|
Among all matching coordinate entries captured by the boundary condition filter, the algorithm targets the specific index $i_{\text{best}}$ that minimizes the spatial sample offset relative to the operator's input selection address ($n_{\text{target}}$):
|
||||||
|
|
||||||
|
$$i_{\text{best}} = \arg\min_{i} \left\vert{} i - n_{\text{target}} \right\vert{}$$
|
||||||
|
|
||||||
|
At coordinate point $i_{\text{best}}$, the immediate signal amplitude approaches zero ($x[i_{\text{best}}] \approx 0$). Slicing at this address ensures absolute physical phase continuity when the audio stream is partitioned or unlinked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Python Backend Implementation Manual (Docker Celery DSP Worker)
|
||||||
|
|
||||||
|
This prototype Python module (`core/ai_dsp_engine.py`) runs on the backend Celery worker environment to execute automated loop indexing and fade-free zero-crossing slicing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import numpy as np
|
||||||
|
import librosa
|
||||||
|
|
||||||
|
class AIDSPEngine:
|
||||||
|
@staticmethod
|
||||||
|
def find_exact_zero_crossing(y: np.ndarray, sr: int, target_time: float, window_ms: float = 50.0) -> float:
|
||||||
|
"""
|
||||||
|
Locates the absolute nearest physical zero-crossing sample index to target_time (seconds).
|
||||||
|
Returns the optimized timeline index position in seconds where amplitude hits 0.
|
||||||
|
"""
|
||||||
|
target_sample = int(target_time * sr)
|
||||||
|
window_samples = int((window_ms / 1000.0) * sr)
|
||||||
|
|
||||||
|
# Define symmetrical horizontal boundary window
|
||||||
|
start_idx = max(0, target_sample - window_samples // 2)
|
||||||
|
end_idx = min(len(y) - 2, target_sample + window_samples // 2)
|
||||||
|
|
||||||
|
y_segment = y[start_idx:end_idx]
|
||||||
|
|
||||||
|
# DSP Condition logic tracking sign inversion: y[i] * y[i+1] <= 0
|
||||||
|
zero_crossings = np.where(y_segment[:-1] * y_segment[1:] <= 0)[0]
|
||||||
|
|
||||||
|
if len(zero_crossings) == 0:
|
||||||
|
# Fallback: if no sign change occurs, return the absolute minimum sample inside the viewport
|
||||||
|
abs_min_idx = np.argmin(np.abs(y_segment))
|
||||||
|
return float((abs_min_idx + start_idx) / sr)
|
||||||
|
|
||||||
|
# Translate local segment array address back to absolute buffer coordinates
|
||||||
|
absolute_crossings = zero_crossings + start_idx
|
||||||
|
|
||||||
|
# Isolate the crossing point closest to the raw target_sample baseline
|
||||||
|
distances = np.abs(absolute_crossings - target_sample)
|
||||||
|
best_sample_idx = absolute_crossings[np.argmin(distances)]
|
||||||
|
|
||||||
|
return float(best_sample_idx / sr)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scan_best_loop_regions(cls, y: np.ndarray, sr: int, min_duration: float = 2.0, max_duration: float = 8.0) -> list:
|
||||||
|
"""
|
||||||
|
Evaluates spectral Self-Similarity Matrices (Recurrence plots) to extract
|
||||||
|
the most musically periodic and cohesive loop segments within the track.
|
||||||
|
"""
|
||||||
|
# 1. Compute harmonic structural properties via Chroma Constant-Q Transform
|
||||||
|
chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
|
||||||
|
|
||||||
|
# 2. Compile the Self-Similarity Matrix (Cosine Recurrence Plot)
|
||||||
|
# This maps global structural recurrence profiles across runtime frame vectors
|
||||||
|
from sklearn.metrics.pairwise import cosine_similarity
|
||||||
|
ssm = cosine_similarity(chroma.T, chroma.T)
|
||||||
|
|
||||||
|
num_frames = ssm.shape[0]
|
||||||
|
hop_length = 512
|
||||||
|
frame_duration = hop_length / sr
|
||||||
|
|
||||||
|
best_score = -1.0
|
||||||
|
best_loop = (0.0, 4.0) # Fallback baseline setup to target initial 4 seconds
|
||||||
|
|
||||||
|
# Scan sub-diagonals to track high-density recurring correlation coefficients
|
||||||
|
# Diagonals parallel to the main identity path flag strict periodic cycles
|
||||||
|
min_frames = int(min_duration / frame_duration)
|
||||||
|
max_frames = int(max_duration / frame_duration)
|
||||||
|
|
||||||
|
for lag in range(min_frames, min_frames * 4): # Trace delay frames matching typical 1-2 measure blocks
|
||||||
|
if lag >= num_frames:
|
||||||
|
break
|
||||||
|
# Accumulate mean recurrence indices across the active sub-diagonal line
|
||||||
|
score = np.mean(np.diagonal(ssm, offset=lag))
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
# Map optimized chronological boundaries
|
||||||
|
start_frame = 0
|
||||||
|
end_frame = min(num_frames - 1, start_frame + lag)
|
||||||
|
|
||||||
|
t_start = start_frame * frame_duration
|
||||||
|
t_end = end_frame * frame_duration
|
||||||
|
|
||||||
|
best_loop = (t_start, t_end)
|
||||||
|
|
||||||
|
# 3. Lock boundaries to precise physical zero-crossings to prevent transient click noise
|
||||||
|
t_start_zero = cls.find_exact_zero_crossing(y, sr, best_loop[0])
|
||||||
|
t_end_zero = cls.find_exact_zero_crossing(y, sr, best_loop[1])
|
||||||
|
|
||||||
|
return [{"start_time": t_start_zero, "end_time": t_end_zero, "score": float(best_score)}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def slice_and_copy_with_zero_crossing(
|
||||||
|
cls,
|
||||||
|
y: np.ndarray,
|
||||||
|
sr: int,
|
||||||
|
start_time: float,
|
||||||
|
end_time: float
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Slices an audio data array from start_time to end_time using zero-crossing alignment.
|
||||||
|
Strictly bypasses linear or exponential fade configurations.
|
||||||
|
"""
|
||||||
|
# Align bounding start and termination boundaries directly to zero-amplitude addresses
|
||||||
|
t_start_zero = cls.find_exact_zero_crossing(y, sr, start_time)
|
||||||
|
t_end_zero = cls.find_exact_zero_crossing(y, sr, end_time)
|
||||||
|
|
||||||
|
sample_start = int(t_start_zero * sr)
|
||||||
|
sample_end = int(t_end_zero * sr)
|
||||||
|
|
||||||
|
# Squeeze out raw buffer array slice without applying any destructive envelope modifiers
|
||||||
|
y_sliced = np.copy(y[sample_start:sample_end])
|
||||||
|
|
||||||
|
return y_sliced, t_start_zero, t_end_zero
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Serialized API Data Transfer Protocols
|
||||||
|
|
||||||
|
During data exchange cycles initiated over the AI Panel UI layer, the client application communicates with the FastAPI routing layer via the following structured JSON payloads:
|
||||||
|
|
||||||
|
### 5.1. API 1: AI Loop Scanning (POST `/api/v1/audio/ai-scan`)
|
||||||
|
|
||||||
|
* **Request Payload (Client $\rightarrow$ Server):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"track_id": "1",
|
||||||
|
"file_id": "creak_forest_raw.wav",
|
||||||
|
"min_loop_duration": 2.0,
|
||||||
|
"max_loop_duration": 6.0
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Response Payload (Server $\rightarrow$ Client):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"track_id": "1",
|
||||||
|
"suggested_loops": [
|
||||||
|
{
|
||||||
|
"start_time": 1.4589,
|
||||||
|
"end_time": 5.4592,
|
||||||
|
"score": 0.892
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Upon parsing this response, the frontend layout engine executes an automated marker rendering pass, pinning visual handles precisely at `start_time` and `end_time`).*
|
||||||
|
|
||||||
|
### 5.2. API 2: Fade-Free AI Slicing (POST `/api/v1/audio/ai-cut`)
|
||||||
|
|
||||||
|
* **Request Payload (Client $\rightarrow$ Server):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source_track_id": "1",
|
||||||
|
"file_id": "creak_forest_raw.wav",
|
||||||
|
"selection_start": 3.120,
|
||||||
|
"selection_end": 7.450
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Response Payload (Server $\rightarrow$ Client):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"output_file_id": "ai_cut_creak_forest_3.1s.wav",
|
||||||
|
"aligned_start": 3.1192,
|
||||||
|
"aligned_end": 7.4504,
|
||||||
|
"duration": 4.3312
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
*(The frontend automatically builds a new track row layout right below the baseline channel, mapping the received `output_file_id` block to mount perfectly at the real-world timeline timestamp indicated by `aligned_start`).*
|
||||||
+4
-4
@@ -60,7 +60,7 @@ def seed_admin():
|
|||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
default_pwd = os.getenv("DEFAULT_ADMIN_PASSWORD", "admin123").strip()
|
default_pwd = (os.getenv("DEFAULT_ADMIN_PASSWORD") or "admin123").strip()
|
||||||
hashed_pwd = hash_password(default_pwd)
|
hashed_pwd = hash_password(default_pwd)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ def seed_admin():
|
|||||||
""", (admin_id,))
|
""", (admin_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
else:
|
else:
|
||||||
# Guarantee admin account password hash matches default_pwd if must_change_password is true or hash doesn't match
|
# Kiểm tra và sửa password admin mặc định nếu cần
|
||||||
if row["must_change_password"] or not verify_password(default_pwd, row["hashed_password"]):
|
if not verify_password(default_pwd, row["hashed_password"]):
|
||||||
cursor.execute("UPDATE users SET hashed_password = ? WHERE id = ?", (hashed_pwd, row["id"]))
|
cursor.execute("UPDATE users SET hashed_password = ?, must_change_password = 1 WHERE id = ?", (hashed_pwd, row["id"]))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ os.makedirs(settings.PROCESSED_DIR, exist_ok=True)
|
|||||||
|
|
||||||
app = FastAPI(title="SonicForge API Engine")
|
app = FastAPI(title="SonicForge API Engine")
|
||||||
|
|
||||||
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
|
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
|
|||||||
Binary file not shown.
+135
-105
@@ -1600,10 +1600,10 @@
|
|||||||
{/* Tools Section - passthrough từ main session */}
|
{/* Tools Section - passthrough từ main session */}
|
||||||
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
|
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
|
||||||
<button className={`p-1 rounded ${activeTool === 'select' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('select')} title="Select Tool">
|
<button className={`p-1 rounded ${activeTool === 'select' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('select')} title="Select Tool">
|
||||||
<i data-lucide="mouse-pointer" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="mouse-pointer" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button className={`p-1 rounded ${activeTool === 'grab' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('grab')} title="Grab Tool">
|
<button className={`p-1 rounded ${activeTool === 'grab' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('grab')} title="Grab Tool">
|
||||||
<i data-lucide="hand" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="hand" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button className={`p-1 rounded ${activeTool === 'razor' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('razor')} title="Razor Tool">
|
<button className={`p-1 rounded ${activeTool === 'razor' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('razor')} title="Razor Tool">
|
||||||
<svg className="w-4 h-4 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
<svg className="w-4 h-4 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
@@ -1613,22 +1613,22 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button className={`p-1 rounded ${activeTool === 'pen' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('pen')} title="Pen Tool">
|
<button className={`p-1 rounded ${activeTool === 'pen' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('pen')} title="Pen Tool">
|
||||||
<i data-lucide="pen-tool" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="pen-tool" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="w-[1px] h-4 bg-zinc-800 mx-0.5"></div>
|
<div className="w-[1px] h-4 bg-zinc-800 mx-0.5"></div>
|
||||||
|
|
||||||
<button onClick={onGlue} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition" title="Glue Clips">
|
<button onClick={onGlue} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition" title="Glue Clips">
|
||||||
<i data-lucide="link" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="link" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onCut} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition" title="Cut (Ctrl+X)">
|
<button onClick={onCut} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition" title="Cut (Ctrl+X)">
|
||||||
<i data-lucide="scissors" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onCopy} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition" title="Copy (Ctrl+C)">
|
<button onClick={onCopy} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition" title="Copy (Ctrl+C)">
|
||||||
<i data-lucide="copy" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onPaste} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition" title="Paste (Ctrl+V)">
|
<button onClick={onPaste} className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition" title="Paste (Ctrl+V)">
|
||||||
<i data-lucide="clipboard" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1683,30 +1683,30 @@
|
|||||||
{/* Transport Controls */}
|
{/* Transport Controls */}
|
||||||
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
|
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
|
||||||
<button onClick={onRewind} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Rewind">
|
<button onClick={onRewind} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Rewind">
|
||||||
<i data-lucide="skip-back" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="skip-back" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onPlayPause} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
|
<button onClick={onPlayPause} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
|
||||||
isPlaying
|
isPlaying
|
||||||
? 'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500'
|
? 'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500'
|
||||||
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
|
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
|
||||||
}`} title={isPlaying ? "Pause" : "Play"}>
|
}`} title={isPlaying ? "Pause" : "Play"}>
|
||||||
<i data-lucide={isPlaying ? 'pause' : 'play'} className="w-4 h-4 fill-current"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide={isPlaying ? 'pause' : 'play'} className="w-4 h-4 fill-current"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onStop} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Stop">
|
<button onClick={onStop} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Stop">
|
||||||
<i data-lucide="square" className="w-4 h-4 fill-current"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="square" className="w-4 h-4 fill-current"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onForward} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Forward">
|
<button onClick={onForward} className="w-8 h-8 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition" title="Forward">
|
||||||
<i data-lucide="skip-forward" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="skip-forward" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onLoop} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
|
<button onClick={onLoop} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
|
||||||
isLooping ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
|
isLooping ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
|
||||||
}`} title={isLooping ? "Loop On" : "Loop Off"}>
|
}`} title={isLooping ? "Loop On" : "Loop Off"}>
|
||||||
<i data-lucide="repeat" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onRecord} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
|
<button onClick={onRecord} className={`w-8 h-8 flex items-center justify-center rounded border transition ${
|
||||||
isRecording ? 'bg-red-600 text-white border-red-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
|
isRecording ? 'bg-red-600 text-white border-red-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
|
||||||
}`} title={isRecording ? "Recording" : "Record"}>
|
}`} title={isRecording ? "Recording" : "Record"}>
|
||||||
<i data-lucide="circle" className="w-4 h-4"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="circle" className="w-4 h-4"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1898,15 +1898,24 @@
|
|||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
useEffect(() => { if (mode) setActiveTab(mode); }, [mode]);
|
useEffect(() => {
|
||||||
|
if (mode) setActiveTab(mode);
|
||||||
|
if (mode === 'force_change' && !oldPassword) {
|
||||||
|
setOldPassword('admin123');
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault(); setError(''); setLoading(true);
|
e.preventDefault(); setError(''); setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (activeTab === 'login') {
|
if (activeTab === 'login') {
|
||||||
const targetUsername = username.trim() || 'admin';
|
const targetUsername = username.trim() || 'admin';
|
||||||
const res = await window.SonicAPI.login(targetUsername, password.trim());
|
const targetPwd = password.trim() || 'admin123';
|
||||||
|
const res = await window.SonicAPI.login(targetUsername, targetPwd);
|
||||||
localStorage.setItem('sonic_token', res.access_token);
|
localStorage.setItem('sonic_token', res.access_token);
|
||||||
localStorage.setItem('sonic_user', JSON.stringify(res.user));
|
localStorage.setItem('sonic_user', JSON.stringify(res.user));
|
||||||
|
if (res.user && res.user.must_change_password) {
|
||||||
|
setOldPassword(targetPwd);
|
||||||
|
}
|
||||||
onSuccess(res.user, res.access_token);
|
onSuccess(res.user, res.access_token);
|
||||||
} else if (activeTab === 'register') {
|
} else if (activeTab === 'register') {
|
||||||
const res = await window.SonicAPI.register(username.trim(), email.trim(), password.trim());
|
const res = await window.SonicAPI.register(username.trim(), email.trim(), password.trim());
|
||||||
@@ -1922,7 +1931,7 @@
|
|||||||
onSuccess(user, res.access_token);
|
onSuccess(user, res.access_token);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Thao tác không thành công');
|
setError(err.message || (activeTab === 'login' ? 'Tài khoản hoặc mật khẩu không chính xác. (Nếu bạn đã đổi mật khẩu trước đó, vui lòng nhập mật khẩu mới mà bạn đã tạo)' : 'Thao tác không thành công'));
|
||||||
} finally { setLoading(false); }
|
} finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
const isForceMode = activeTab === 'force_change';
|
const isForceMode = activeTab === 'force_change';
|
||||||
@@ -1980,6 +1989,15 @@
|
|||||||
<button type="submit" disabled={loading} className="w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150">
|
<button type="submit" disabled={loading} className="w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150">
|
||||||
{loading ? 'Đang xác thực...' : (isForceMode ? 'Đổi Mật Khẩu Ngay' : (activeTab === 'login' ? 'Đăng Nhập System' : 'Tạo Tài Khoản Mới'))}
|
{loading ? 'Đang xác thực...' : (isForceMode ? 'Đổi Mật Khẩu Ngay' : (activeTab === 'login' ? 'Đăng Nhập System' : 'Tạo Tài Khoản Mới'))}
|
||||||
</button>
|
</button>
|
||||||
|
{activeTab === 'login' && !isForceMode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setUsername('admin'); setPassword('admin123'); setError(''); }}
|
||||||
|
className="w-full mt-2 py-1.5 bg-amber-950/60 hover:bg-amber-900/80 text-amber-300 border border-amber-800/60 text-xs font-semibold rounded-lg transition flex items-center justify-center gap-1.5"
|
||||||
|
>
|
||||||
|
🔑 Điền Nhanh Tài Khoản Admin (admin / admin123)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
{!isForceMode && (
|
{!isForceMode && (
|
||||||
<div className="mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400">
|
<div className="mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400">
|
||||||
@@ -2333,7 +2351,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Tab System (LOOP_EDITOR_2.md §1) ──
|
// ── Tab System (LOOP_EDITOR_2.md §1) ──
|
||||||
const [activeTab, setActiveTab] = useState('subtab_1');
|
const [activeTab, setActiveTab] = useState('main');
|
||||||
const [subTabSelectedNodeTime, setSubTabSelectedNodeTime] = useState(null);
|
const [subTabSelectedNodeTime, setSubTabSelectedNodeTime] = useState(null);
|
||||||
const [subTabNormVal, setSubTabNormVal] = useState(0);
|
const [subTabNormVal, setSubTabNormVal] = useState(0);
|
||||||
const [subTabGainVal, setSubTabGainVal] = useState(100);
|
const [subTabGainVal, setSubTabGainVal] = useState(100);
|
||||||
@@ -2790,6 +2808,10 @@
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e) => {
|
const handler = (e) => {
|
||||||
|
// Bypass global hotkeys when typing inside input/textarea/contentEditable elements
|
||||||
|
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const ctrl = e.ctrlKey || e.metaKey;
|
const ctrl = e.ctrlKey || e.metaKey;
|
||||||
const alt = e.altKey;
|
const alt = e.altKey;
|
||||||
|
|
||||||
@@ -5762,7 +5784,7 @@
|
|||||||
) : (
|
) : (
|
||||||
<button key={item.label} onClick={(e) => { e.stopPropagation(); item.action(); setMenuOpen(null); }}
|
<button key={item.label} onClick={(e) => { e.stopPropagation(); item.action(); setMenuOpen(null); }}
|
||||||
className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide={item.icon} className="w-3.5 h-3.5 text-zinc-500 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide={item.icon} className="w-3.5 h-3.5 text-zinc-500 shrink-0"></i></span>
|
||||||
<span className="flex-1">{item.label}</span>
|
<span className="flex-1">{item.label}</span>
|
||||||
{item.shortcut && <span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">{item.shortcut}</span>}
|
{item.shortcut && <span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">{item.shortcut}</span>}
|
||||||
</button>
|
</button>
|
||||||
@@ -5782,7 +5804,7 @@
|
|||||||
className={`px-1.5 py-0.5 rounded text-[10px] border transition ${
|
className={`px-1.5 py-0.5 rounded text-[10px] border transition ${
|
||||||
showAIConfig ? 'bg-purple-900 text-purple-200 border-purple-700' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'
|
showAIConfig ? 'bg-purple-900 text-purple-200 border-purple-700' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'
|
||||||
}`}>
|
}`}>
|
||||||
<i data-lucide="cpu" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-3 h-3"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -5797,7 +5819,7 @@
|
|||||||
? 'text-cyan-400 border-cyan-500 bg-zinc-800/50'
|
? 'text-cyan-400 border-cyan-500 bg-zinc-800/50'
|
||||||
: 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'
|
: 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'
|
||||||
}`}>
|
}`}>
|
||||||
<i data-lucide="layout-dashboard" className="w-3 h-3"></i> Main Session
|
<span className="inline-flex items-center shrink-0"><i data-lucide="layout-dashboard" className="w-3 h-3"></i></span> Main Session
|
||||||
</button>
|
</button>
|
||||||
{subTabs.map(st => (
|
{subTabs.map(st => (
|
||||||
<div key={st.id} className="flex items-stretch">
|
<div key={st.id} className="flex items-stretch">
|
||||||
@@ -5807,13 +5829,13 @@
|
|||||||
? 'text-amber-400 border-amber-500 bg-zinc-800/50'
|
? 'text-amber-400 border-amber-500 bg-zinc-800/50'
|
||||||
: 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'
|
: 'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'
|
||||||
}`}>
|
}`}>
|
||||||
<i data-lucide="file-edit" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="file-edit" className="w-3 h-3"></i></span>
|
||||||
<span className="max-w-[100px] truncate">{st.label}</span>
|
<span className="max-w-[100px] truncate">{st.label}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => closeSubTab(st.id)}
|
<button onClick={() => closeSubTab(st.id)}
|
||||||
className="px-1 text-zinc-600 hover:text-red-400 transition text-[9px]"
|
className="px-1 text-zinc-600 hover:text-red-400 transition text-[9px]"
|
||||||
title="Close tab">
|
title="Close tab">
|
||||||
<i data-lucide="x" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -5823,7 +5845,7 @@
|
|||||||
{showAIConfig && (
|
{showAIConfig && (
|
||||||
<div className="bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all">
|
<div className="bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all">
|
||||||
<div className="text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1">
|
<div className="text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1">
|
||||||
<i data-lucide="cpu" className="w-4 h-4"></i> Cấu hình cổng kết nối API
|
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-4 h-4"></i></span> Cấu hình cổng kết nối API
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -5866,17 +5888,17 @@
|
|||||||
<div className="flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg"
|
<div className="flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg"
|
||||||
title="Kéo để di chuyển toolbar" style={{ cursor: 'grab' }}>
|
title="Kéo để di chuyển toolbar" style={{ cursor: 'grab' }}>
|
||||||
<div className="flex items-center gap-0.5 mr-1 text-zinc-600">
|
<div className="flex items-center gap-0.5 mr-1 text-zinc-600">
|
||||||
<i data-lucide="grip-vertical" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => { setActiveTool('select'); showToast('Select Tool', 'info'); }}
|
<button onClick={() => { setActiveTool('select'); showToast('Select Tool', 'info'); }}
|
||||||
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'select' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
|
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'select' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
|
||||||
title="Select Tool (V)">
|
title="Select Tool (V)">
|
||||||
<i data-lucide="mouse-pointer" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="mouse-pointer" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => { setActiveTool('grab'); showToast('Grab Tool', 'info'); }}
|
<button onClick={() => { setActiveTool('grab'); showToast('Grab Tool', 'info'); }}
|
||||||
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'grab' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
|
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'grab' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
|
||||||
title="Grab Tool (H)">
|
title="Grab Tool (H)">
|
||||||
<i data-lucide="hand" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="hand" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5">
|
<div className="flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5">
|
||||||
<button onClick={() => { setActiveTool('razor'); showToast('Razor Tool', 'info'); }}
|
<button onClick={() => { setActiveTool('razor'); showToast('Razor Tool', 'info'); }}
|
||||||
@@ -5891,40 +5913,47 @@
|
|||||||
<button onClick={handleGlueTracks}
|
<button onClick={handleGlueTracks}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800"
|
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800"
|
||||||
title="Glue Clips">
|
title="Glue Clips">
|
||||||
<i data-lucide="link" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="link" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => { setActiveTool('pen'); showToast('Pen Tool', 'info'); }}
|
<button onClick={() => { setActiveTool('pen'); showToast('Pen Tool', 'info'); }}
|
||||||
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'pen' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
|
className={`w-7 h-7 flex items-center justify-center rounded ${activeTool === 'pen' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
|
||||||
title="Pen Tool (P)">
|
title="Pen Tool (P)">
|
||||||
<i data-lucide="pen-tool" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="pen-tool" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
|
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
|
||||||
<button onClick={handleCutTrack}
|
<button onClick={handleCutTrack}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800"
|
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800"
|
||||||
title="Cut (Ctrl+X)">
|
title="Cut (Ctrl+X)">
|
||||||
<i data-lucide="scissors" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleCopyTrack}
|
<button onClick={handleCopyTrack}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800"
|
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800"
|
||||||
title="Copy (Ctrl+C)">
|
title="Copy (Ctrl+C)">
|
||||||
<i data-lucide="copy" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handlePasteTrack}
|
<button onClick={handlePasteTrack}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800"
|
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800"
|
||||||
title="Paste (Ctrl+V)">
|
title="Paste (Ctrl+V)">
|
||||||
<i data-lucide="clipboard" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5"></i></span>
|
||||||
|
</button>
|
||||||
|
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
|
||||||
|
<button onClick={addNewTrack}
|
||||||
|
className="px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition"
|
||||||
|
title="Thêm Track Mới (Ctrl+I)">
|
||||||
|
<span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3.5 h-3.5"></i></span>
|
||||||
|
<span>Track</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
|
<div className="w-[1px] h-5 bg-zinc-700 mx-0.5"></div>
|
||||||
<button onClick={handleUndo} disabled={undoStack.length === 0}
|
<button onClick={handleUndo} disabled={undoStack.length === 0}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30"
|
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30"
|
||||||
title="Undo (Ctrl+Z)">
|
title="Undo (Ctrl+Z)">
|
||||||
<i data-lucide="undo" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="undo" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleRedo} disabled={redoStack.length === 0}
|
<button onClick={handleRedo} disabled={redoStack.length === 0}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30"
|
className="w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30"
|
||||||
title="Redo (Ctrl+Y)">
|
title="Redo (Ctrl+Y)">
|
||||||
<i data-lucide="redo" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="redo" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -5938,7 +5967,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
||||||
title="Quay lại đầu"><i data-lucide="skip-back" className="w-3.5 h-3.5"></i></button>
|
title="Quay lại đầu"><span className="inline-flex items-center shrink-0"><i data-lucide="skip-back" className="w-3.5 h-3.5"></i></span></button>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
if (activeTab !== 'main') {
|
if (activeTab !== 'main') {
|
||||||
setSubTabs(prev => prev.map(s => {
|
setSubTabs(prev => prev.map(s => {
|
||||||
@@ -5951,7 +5980,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
||||||
title="Đầu vùng chọn"><i data-lucide="step-back" className="w-3.5 h-3.5"></i></button>
|
title="Đầu vùng chọn"><span className="inline-flex items-center shrink-0"><i data-lucide="step-back" className="w-3.5 h-3.5"></i></span></button>
|
||||||
<button onClick={handlePlayPause}
|
<button onClick={handlePlayPause}
|
||||||
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
|
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
|
||||||
isPlaying
|
isPlaying
|
||||||
@@ -5959,11 +5988,11 @@
|
|||||||
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
|
: 'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'
|
||||||
}`}
|
}`}
|
||||||
title={isPlaying ? "Tạm dừng" : "Play"}>
|
title={isPlaying ? "Tạm dừng" : "Play"}>
|
||||||
<i data-lucide={isPlaying ? "pause" : "play"} className="w-3.5 h-3.5 fill-current"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide={isPlaying ? "pause" : "play"} className="w-3.5 h-3.5 fill-current"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleStop}
|
<button onClick={handleStop}
|
||||||
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
||||||
title="Stop"><i data-lucide="square" className="w-3.5 h-3.5 fill-current"></i></button>
|
title="Stop"><span className="inline-flex items-center shrink-0"><i data-lucide="square" className="w-3.5 h-3.5 fill-current"></i></span></button>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
if (activeTab !== 'main') {
|
if (activeTab !== 'main') {
|
||||||
setSubTabs(prev => prev.map(s => {
|
setSubTabs(prev => prev.map(s => {
|
||||||
@@ -5976,7 +6005,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
||||||
title="Cuối vùng chọn"><i data-lucide="step-forward" className="w-3.5 h-3.5"></i></button>
|
title="Cuối vùng chọn"><span className="inline-flex items-center shrink-0"><i data-lucide="step-forward" className="w-3.5 h-3.5"></i></span></button>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
if (activeTab !== 'main') {
|
if (activeTab !== 'main') {
|
||||||
setSubTabs(prev => prev.map(s => {
|
setSubTabs(prev => prev.map(s => {
|
||||||
@@ -5989,7 +6018,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
|
||||||
title="Đến cuối"><i data-lucide="skip-forward" className="w-3.5 h-3.5"></i></button>
|
title="Đến cuối"><span className="inline-flex items-center shrink-0"><i data-lucide="skip-forward" className="w-3.5 h-3.5"></i></span></button>
|
||||||
<div className="w-[1px] h-5 bg-zinc-800 mx-0.5"></div>
|
<div className="w-[1px] h-5 bg-zinc-800 mx-0.5"></div>
|
||||||
<button onClick={() => setIsLoopingSelection(prev => !prev)}
|
<button onClick={() => setIsLoopingSelection(prev => !prev)}
|
||||||
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
|
className={`w-7 h-7 flex items-center justify-center rounded border transition ${
|
||||||
@@ -5997,7 +6026,7 @@
|
|||||||
? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500'
|
? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500'
|
||||||
: 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
|
: 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
|
||||||
}`} title="Bật/Tắt Lặp vùng chọn">
|
}`} title="Bật/Tắt Lặp vùng chọn">
|
||||||
<i data-lucide="repeat" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<span className="text-[14px] text-zinc-500 font-bold uppercase ml-2">Snap</span>
|
<span className="text-[14px] text-zinc-500 font-bold uppercase ml-2">Snap</span>
|
||||||
<select value={snapValue} onChange={e => setSnapValue(e.target.value)}
|
<select value={snapValue} onChange={e => setSnapValue(e.target.value)}
|
||||||
@@ -6087,10 +6116,10 @@
|
|||||||
<div className="flex flex-col h-full gap-1.5">
|
<div className="flex flex-col h-full gap-1.5">
|
||||||
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('export', e)}>
|
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('export', e)}>
|
||||||
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
|
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
|
||||||
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span>
|
||||||
<i data-lucide="save" className="w-3.5 h-3.5 text-cyan-400"></i> Export
|
<span className="inline-flex items-center shrink-0"><i data-lucide="save" className="w-3.5 h-3.5 text-cyan-400"></i></span> Export
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={() => closePanel('export')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
|
<button onClick={() => closePanel('export')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1">
|
<div className="grid grid-cols-3 gap-1">
|
||||||
<div>
|
<div>
|
||||||
@@ -6117,7 +6146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button onClick={triggerWavExport} disabled={isExporting}
|
<button onClick={triggerWavExport} disabled={isExporting}
|
||||||
className="w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-[10px] flex items-center justify-center gap-1">
|
className="w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-[10px] flex items-center justify-center gap-1">
|
||||||
<i data-lucide="download-cloud" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="download-cloud" className="w-3 h-3"></i></span>
|
||||||
{isExporting ? '...' : 'Export'}
|
{isExporting ? '...' : 'Export'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -6126,10 +6155,10 @@
|
|||||||
<div className="flex flex-col h-full gap-1.5">
|
<div className="flex flex-col h-full gap-1.5">
|
||||||
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('ai', e)}>
|
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('ai', e)}>
|
||||||
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
|
<h3 className="font-bold text-[10px] text-zinc-200 flex items-center gap-1">
|
||||||
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span>
|
||||||
<i data-lucide="cpu" className="w-3.5 h-3.5 text-purple-400"></i> AI
|
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-3.5 h-3.5 text-purple-400"></i></span> AI
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={() => closePanel('ai')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
|
<button onClick={() => closePanel('ai')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-1.5 bg-[#141414] rounded border border-zinc-800 text-[9px] font-mono min-h-[24px]">
|
<div className="p-1.5 bg-[#141414] rounded border border-zinc-800 text-[9px] font-mono min-h-[24px]">
|
||||||
<div className="text-zinc-500">// <span className="text-zinc-300">{analysisState.status}</span></div>
|
<div className="text-zinc-500">// <span className="text-zinc-300">{analysisState.status}</span></div>
|
||||||
@@ -6137,13 +6166,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1">
|
<div className="grid grid-cols-3 gap-1">
|
||||||
<button onClick={handleMarkSelection} className="py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-[9px] border border-purple-700 flex items-center justify-center gap-1">
|
<button onClick={handleMarkSelection} className="py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-[9px] border border-purple-700 flex items-center justify-center gap-1">
|
||||||
<i data-lucide="map-pin" className="w-3 h-3"></i> Mark
|
<span className="inline-flex items-center shrink-0"><i data-lucide="map-pin" className="w-3 h-3"></i></span> Mark
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleAICutToNewTrack} disabled={analysisState.isRunning} className="py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-[9px] flex items-center justify-center gap-1">
|
<button onClick={handleAICutToNewTrack} disabled={analysisState.isRunning} className="py-1 bg-purple-700 hover:bg-purple-600 text-white font-bold rounded text-[9px] flex items-center justify-center gap-1">
|
||||||
<i data-lucide="scissors" className="w-3 h-3"></i> AI Cut
|
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3 h-3"></i></span> AI Cut
|
||||||
</button>
|
</button>
|
||||||
<button onClick={triggerAIAnalysis} disabled={analysisState.isRunning} className="py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 font-bold rounded text-[9px] border border-zinc-600 flex items-center justify-center gap-1">
|
<button onClick={triggerAIAnalysis} disabled={analysisState.isRunning} className="py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 font-bold rounded text-[9px] border border-zinc-600 flex items-center justify-center gap-1">
|
||||||
<i data-lucide="sparkles" className="w-3 h-3"></i> Analyze
|
<span className="inline-flex items-center shrink-0"><i data-lucide="sparkles" className="w-3 h-3"></i></span> Analyze
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -6152,9 +6181,9 @@
|
|||||||
<div className="flex flex-col h-full gap-1.5">
|
<div className="flex flex-col h-full gap-1.5">
|
||||||
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('selection', e)}>
|
<div className="flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none" onMouseDown={e => startPanelDrag('selection', e)}>
|
||||||
<span className="text-[10px] text-zinc-500 font-bold uppercase flex items-center gap-1">
|
<span className="text-[10px] text-zinc-500 font-bold uppercase flex items-center gap-1">
|
||||||
<i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i> Selection
|
<span className="inline-flex items-center shrink-0"><i data-lucide="grip-vertical" className="w-3 h-3 text-zinc-500"></i></span> Selection
|
||||||
</span>
|
</span>
|
||||||
<button onClick={() => closePanel('selection')} className="text-zinc-600 hover:text-zinc-300"><i data-lucide="x" className="w-3 h-3"></i></button>
|
<button onClick={() => closePanel('selection')} className="text-zinc-600 hover:text-zinc-300"><span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800">
|
<div className="grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800">
|
||||||
<div>
|
<div>
|
||||||
@@ -6222,25 +6251,9 @@
|
|||||||
{renderPanelContent(p)}
|
{renderPanelContent(p)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* ── Auth & User Modals ── */}
|
</div>
|
||||||
<AuthModal
|
);
|
||||||
isOpen={authModalOpen}
|
};
|
||||||
mode={authMode}
|
|
||||||
forceMandatory={isMandatoryLogin}
|
|
||||||
onClose={() => setAuthModalOpen(false)}
|
|
||||||
onSuccess={handleAuthSuccess}
|
|
||||||
/>
|
|
||||||
<ProfileModal
|
|
||||||
isOpen={profileModalOpen}
|
|
||||||
onClose={() => setProfileModalOpen(false)}
|
|
||||||
/>
|
|
||||||
<SystemManagerModal
|
|
||||||
isOpen={systemManagerModalOpen}
|
|
||||||
onClose={() => setSystemManagerModalOpen(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={workspaceRef} className="flex-1 flex flex-col overflow-hidden select-none daw-bg relative">
|
<div ref={workspaceRef} className="flex-1 flex flex-col overflow-hidden select-none daw-bg relative">
|
||||||
@@ -6257,7 +6270,7 @@
|
|||||||
<div className="fixed z-[100] pointer-events-none opacity-80 border border-cyan-500 rounded-lg bg-[#262626] shadow-2xl p-3 w-56"
|
<div className="fixed z-[100] pointer-events-none opacity-80 border border-cyan-500 rounded-lg bg-[#262626] shadow-2xl p-3 w-56"
|
||||||
style={{ left: dragGhostPos.x, top: dragGhostPos.y }}>
|
style={{ left: dragGhostPos.x, top: dragGhostPos.y }}>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-zinc-200 font-bold">
|
<div className="flex items-center gap-2 text-[10px] text-zinc-200 font-bold">
|
||||||
<i data-lucide="move" className="w-3.5 h-3.5 text-cyan-400"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="move" className="w-3.5 h-3.5 text-cyan-400"></i></span>
|
||||||
{dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : 'Selection Panel'}
|
{dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : 'Selection Panel'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[8px] text-zinc-500 mt-1">Drop at edge to dock</div>
|
<div className="text-[8px] text-zinc-500 mt-1">Drop at edge to dock</div>
|
||||||
@@ -6275,7 +6288,14 @@
|
|||||||
onScroll={handleTCPScroll}
|
onScroll={handleTCPScroll}
|
||||||
className="w-[300px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
|
className="w-[300px] shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900"
|
||||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
||||||
<div className="sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0">
|
<div className="sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0">
|
||||||
|
<span className="text-xs font-bold text-zinc-300 flex items-center gap-1.5">
|
||||||
|
<span className="inline-flex items-center shrink-0"><i data-lucide="sliders" className="w-3.5 h-3.5 text-cyan-400"></i></span>
|
||||||
|
TRACKS ({tracks.length})
|
||||||
|
</span>
|
||||||
|
<button onClick={addNewTrack} className="px-2.5 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-[11px] font-semibold flex items-center gap-1 shadow transition">
|
||||||
|
<span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3 h-3"></i></span> Add Track
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 border-l-4 border-purple-500 shrink-0">
|
<div className="sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 border-l-4 border-purple-500 shrink-0">
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
@@ -6292,7 +6312,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]">
|
<div className="flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]">
|
||||||
{tracks.map((track, idx) => {
|
{tracks.length === 0 ? (
|
||||||
|
<div className="p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3">
|
||||||
|
<span className="inline-flex items-center shrink-0"><i data-lucide="plus-circle" className="w-8 h-8 text-cyan-400 opacity-80"></i></span>
|
||||||
|
<p className="text-xs font-medium">Chưa có Track nào trong dự án.</p>
|
||||||
|
<button onClick={addNewTrack} className="px-3 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 shadow">
|
||||||
|
<span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3.5 h-3.5"></i></span> Thêm Track Mới
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
tracks.map((track, idx) => {
|
||||||
const isSelected = selectedTrackId === track.id;
|
const isSelected = selectedTrackId === track.id;
|
||||||
return (
|
return (
|
||||||
<div key={track.id} style={{ height: `${track.height || 96}px` }}
|
<div key={track.id} style={{ height: `${track.height || 96}px` }}
|
||||||
@@ -6326,7 +6355,7 @@
|
|||||||
<button onClick={e => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
|
<button onClick={e => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
|
||||||
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>S</button>
|
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>S</button>
|
||||||
<button onClick={e => { e.stopPropagation(); deleteTrack(track.id); }}
|
<button onClick={e => { e.stopPropagation(); deleteTrack(track.id); }}
|
||||||
className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button>
|
className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"><span className="inline-flex items-center shrink-0"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[10px]" onClick={e => e.stopPropagation()}>
|
<div className="flex flex-col gap-0.5 text-[10px]" onClick={e => e.stopPropagation()}>
|
||||||
@@ -6343,15 +6372,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 mt-1" onClick={e => e.stopPropagation()}>
|
<div className="flex items-center gap-1.5 mt-1" onClick={e => e.stopPropagation()}>
|
||||||
<input type="file" id={`upload-${track.id}`} accept="audio/*" className="hidden" onChange={e => loadFileOnTrack(track.id, e.target.files[0])} />
|
<input type="file" id={`upload-${track.id}`} accept="audio/*" className="hidden" onChange={e => loadFileOnTrack(track.id, e.target.files[0])} />
|
||||||
<label htmlFor={`upload-${track.id}`} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer border border-zinc-700 font-medium"><i data-lucide="upload" className="w-3 h-3"></i> File</label>
|
<label htmlFor={`upload-${track.id}`} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer border border-zinc-700 font-medium"><span className="inline-flex items-center shrink-0"><i data-lucide="upload" className="w-3 h-3"></i></span> File</label>
|
||||||
<button onClick={e => { e.stopPropagation(); showToast('FX panel for track ' + track.id, 'info'); }} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-purple-400 border border-purple-900 rounded text-[10px] font-bold flex items-center gap-1"><i data-lucide="wand-2" className="w-3 h-3"></i> FX: <span className="text-zinc-500 font-normal">None</span></button>
|
<button onClick={e => { e.stopPropagation(); showToast('FX panel for track ' + track.id, 'info'); }} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-purple-400 border border-purple-900 rounded text-[10px] font-bold flex items-center gap-1"><span className="inline-flex items-center shrink-0"><i data-lucide="wand-2" className="w-3 h-3"></i></span> FX: <span className="text-zinc-500 font-normal">None</span></button>
|
||||||
<button onClick={() => generateSynthToTrack(track.id, 'synth')} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-[10px] font-bold flex items-center gap-1"><i data-lucide="music" className="w-3 h-3"></i> Synth: <span className="text-zinc-500 font-normal">None</span></button>
|
<button onClick={() => generateSynthToTrack(track.id, 'synth')} className="px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-[10px] font-bold flex items-center gap-1"><span className="inline-flex items-center shrink-0"><i data-lucide="music" className="w-3 h-3"></i></span> Synth: <span className="text-zinc-500 font-normal">None</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
|
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
|
||||||
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
|
className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0" />
|
<div className="h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
@@ -6402,7 +6432,7 @@
|
|||||||
{track.buffer && (
|
{track.buffer && (
|
||||||
<div className="absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100">
|
<div className="absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100">
|
||||||
<button onClick={() => handleSplitTrack(track.id)}
|
<button onClick={() => handleSplitTrack(track.id)}
|
||||||
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"><i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i> Cắt</button>
|
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"><span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i></span> Cắt</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
|
<div onMouseDown={e => handleTrackResizeMouseDown(e, track.id)}
|
||||||
@@ -6413,7 +6443,7 @@
|
|||||||
<div className="h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800"
|
<div className="h-[48px] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800"
|
||||||
onMouseEnter={() => { if (draggedClipRef.current) { setHoveredTrackId(addNewTrack()); } }}
|
onMouseEnter={() => { if (draggedClipRef.current) { setHoveredTrackId(addNewTrack()); } }}
|
||||||
onClick={addNewTrack}>
|
onClick={addNewTrack}>
|
||||||
<span className="flex items-center gap-1 text-zinc-400"><i data-lucide="plus" className="w-3.5 h-3.5"></i> Kéo clip xuống hoặc Click tạo Track</span>
|
<span className="flex items-center gap-1 text-zinc-400"><span className="inline-flex items-center shrink-0"><i data-lucide="plus" className="w-3.5 h-3.5"></i></span> Kéo clip xuống hoặc Click tạo Track</span>
|
||||||
</div>
|
</div>
|
||||||
{selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
|
{selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
|
||||||
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
|
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
|
||||||
@@ -6447,7 +6477,7 @@
|
|||||||
<span className="text-[10px] font-bold text-zinc-500 uppercase">Sub-Tab</span>
|
<span className="text-[10px] font-bold text-zinc-500 uppercase">Sub-Tab</span>
|
||||||
<button onClick={() => closeSubTab(st.id)}
|
<button onClick={() => closeSubTab(st.id)}
|
||||||
className="px-1.5 py-0.5 text-[9px] bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700 flex items-center gap-1">
|
className="px-1.5 py-0.5 text-[9px] bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700 flex items-center gap-1">
|
||||||
<i data-lucide="x" className="w-3 h-3"></i> Close
|
<span className="inline-flex items-center shrink-0"><i data-lucide="x" className="w-3 h-3"></i></span> Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{vTrack ? (
|
{vTrack ? (
|
||||||
@@ -6493,11 +6523,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-[14px]">
|
<div className="flex items-center gap-1.5 text-[14px]">
|
||||||
<button onClick={() => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, isLooping: !s.isLooping} : s))}
|
<button onClick={() => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, isLooping: !s.isLooping} : s))}
|
||||||
className={`px-2 py-0.5 text-[14px] rounded border font-bold ${st.isLooping ? 'bg-amber-700 text-white border-amber-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`}><i data-lucide="repeat" className="w-3.5 h-3.5"></i></button>
|
className={`px-2 py-0.5 text-[14px] rounded border font-bold ${st.isLooping ? 'bg-amber-700 text-white border-amber-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'}`}><span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-3.5 h-3.5"></i></span></button>
|
||||||
<button onClick={() => updateSubTabEffects(st.id, { reverse: !((st.effects || {}).reverse) })}
|
<button onClick={() => updateSubTabEffects(st.id, { reverse: !((st.effects || {}).reverse) })}
|
||||||
className={`px-2 py-0.5 rounded border text-[14px] font-bold ${(st.effects || {}).reverse ? 'bg-zinc-600 text-white border-zinc-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'} flex items-center justify-center`}
|
className={`px-2 py-0.5 rounded border text-[14px] font-bold ${(st.effects || {}).reverse ? 'bg-zinc-600 text-white border-zinc-500' : 'bg-zinc-800 text-zinc-400 border-zinc-700'} flex items-center justify-center`}
|
||||||
title="Reverse">
|
title="Reverse">
|
||||||
<i data-lucide="arrow-left-right" className="w-3.5 h-3.5"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="arrow-left-right" className="w-3.5 h-3.5"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<span className="w-10 text-right text-zinc-500 text-[14px]">Loop:</span>
|
<span className="w-10 text-right text-zinc-500 text-[14px]">Loop:</span>
|
||||||
<input type="number" min="0" max="999" value={st.loopCount || 0} onChange={e => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, loopCount: Math.max(0, parseInt(e.target.value) || 0)} : s))} className="w-12 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" title="Loop count" />
|
<input type="number" min="0" max="999" value={st.loopCount || 0} onChange={e => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, loopCount: Math.max(0, parseInt(e.target.value) || 0)} : s))} className="w-12 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" title="Loop count" />
|
||||||
@@ -6538,11 +6568,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<button onClick={() => exportSubTabBuffer(st.id)}
|
<button onClick={() => exportSubTabBuffer(st.id)}
|
||||||
className="w-full py-1.5 mb-1.5 bg-blue-700 hover:bg-blue-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition">
|
className="w-full py-1.5 mb-1.5 bg-blue-700 hover:bg-blue-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition">
|
||||||
<i data-lucide="download" className="w-4 h-4"></i> Export
|
<span className="inline-flex items-center shrink-0"><i data-lucide="download" className="w-4 h-4"></i></span> Export
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => applySubTab(st.id)}
|
<button onClick={() => applySubTab(st.id)}
|
||||||
className="w-full py-1.5 bg-emerald-700 hover:bg-emerald-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition">
|
className="w-full py-1.5 bg-emerald-700 hover:bg-emerald-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition">
|
||||||
<i data-lucide="save" className="w-4 h-4"></i> Save
|
<span className="inline-flex items-center shrink-0"><i data-lucide="save" className="w-4 h-4"></i></span> Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -6672,28 +6702,28 @@
|
|||||||
<button onClick={() => setShowExportPanel(p => !p)}
|
<button onClick={() => setShowExportPanel(p => !p)}
|
||||||
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showExportPanel ? 'bg-cyan-900 text-cyan-300' : 'text-zinc-500 hover:text-zinc-300'}`}
|
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showExportPanel ? 'bg-cyan-900 text-cyan-300' : 'text-zinc-500 hover:text-zinc-300'}`}
|
||||||
title={`Export Panel (${panelPositions.export})`}>
|
title={`Export Panel (${panelPositions.export})`}>
|
||||||
<i data-lucide="save" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="save" className="w-3 h-3"></i></span>
|
||||||
<span className="text-[7px] opacity-60">{showExportPanel ? panelPositions.export[0].toUpperCase() : ''}</span>
|
<span className="text-[7px] opacity-60">{showExportPanel ? panelPositions.export[0].toUpperCase() : ''}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setShowSelectionPanel(p => !p)}
|
<button onClick={() => setShowSelectionPanel(p => !p)}
|
||||||
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showSelectionPanel ? 'bg-amber-900 text-amber-300' : 'text-zinc-500 hover:text-zinc-300'}`}
|
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showSelectionPanel ? 'bg-amber-900 text-amber-300' : 'text-zinc-500 hover:text-zinc-300'}`}
|
||||||
title={`Selection Panel (${panelPositions.selection})`}>
|
title={`Selection Panel (${panelPositions.selection})`}>
|
||||||
<i data-lucide="sliders" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="sliders" className="w-3 h-3"></i></span>
|
||||||
<span className="text-[7px] opacity-60">{showSelectionPanel ? panelPositions.selection[0].toUpperCase() : ''}</span>
|
<span className="text-[7px] opacity-60">{showSelectionPanel ? panelPositions.selection[0].toUpperCase() : ''}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setShowAIPanel(p => !p)}
|
<button onClick={() => setShowAIPanel(p => !p)}
|
||||||
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showAIPanel ? 'bg-purple-900 text-purple-300' : 'text-zinc-500 hover:text-zinc-300'}`}
|
className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showAIPanel ? 'bg-purple-900 text-purple-300' : 'text-zinc-500 hover:text-zinc-300'}`}
|
||||||
title={`AI Panel (${panelPositions.ai})`}>
|
title={`AI Panel (${panelPositions.ai})`}>
|
||||||
<i data-lucide="cpu" className="w-3 h-3"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="cpu" className="w-3 h-3"></i></span>
|
||||||
<span className="text-[7px] opacity-60">{showAIPanel ? panelPositions.ai[0].toUpperCase() : ''}</span>
|
<span className="text-[7px] opacity-60">{showAIPanel ? panelPositions.ai[0].toUpperCase() : ''}</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="w-[1px] h-3 bg-zinc-800 mx-1"></span>
|
<span className="w-[1px] h-3 bg-zinc-800 mx-1"></span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<i data-lucide="info" className="w-3 h-3 text-zinc-600"></i> Scroll: Zoom
|
<span className="inline-flex items-center shrink-0"><i data-lucide="info" className="w-3 h-3 text-zinc-600"></i></span> Scroll: Zoom
|
||||||
</span>
|
</span>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<i data-lucide="keyboard" className="w-3 h-3 text-zinc-600"></i> Ctrl+Scroll: Playhead
|
<span className="inline-flex items-center shrink-0"><i data-lucide="keyboard" className="w-3 h-3 text-zinc-600"></i></span> Ctrl+Scroll: Playhead
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -6708,28 +6738,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-px bg-zinc-700 my-1"></div>
|
<div className="h-px bg-zinc-700 my-1"></div>
|
||||||
<button onClick={() => { handleSubTabCut(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={() => { handleSubTabCut(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Cut</span>
|
<span className="flex-1">Cut</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+X</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+X</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => { handleSubTabCopy(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={() => { handleSubTabCopy(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Copy</span>
|
<span className="flex-1">Copy</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+C</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+C</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => { handleSubTabPaste(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={() => { handleSubTabPaste(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Paste</span>
|
<span className="flex-1">Paste</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+V</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+V</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => { handleSubTabDelete(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={() => { handleSubTabDelete(contextMenu.subTabId); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Delete Selected Segment</span>
|
<span className="flex-1">Delete Selected Segment</span>
|
||||||
<span className="text-amber-400 text-[12px] font-semibold font-mono ml-auto">Del</span>
|
<span className="text-amber-400 text-[12px] font-semibold font-mono ml-auto">Del</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-zinc-700 my-1"></div>
|
<div className="h-px bg-zinc-700 my-1"></div>
|
||||||
<button onClick={() => { handleSubTabLoop(contextMenu.subTabId, 4); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={() => { handleSubTabLoop(contextMenu.subTabId, 4); closeContextMenu(); }} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="repeat" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="repeat" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Loop Selection 4 times</span>
|
<span className="flex-1">Loop Selection 4 times</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+L</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto">Ctrl+L</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -6738,39 +6768,39 @@
|
|||||||
<div className="fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64" style={{ left: contextMenu.x, top: contextMenu.y }}
|
<div className="fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64" style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||||
onClick={(e) => e.stopPropagation()}>
|
onClick={(e) => e.stopPropagation()}>
|
||||||
<button onClick={contextMenuEdit} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={contextMenuEdit} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="file-edit" className="w-3.5 h-3.5 text-amber-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="file-edit" className="w-3.5 h-3.5 text-amber-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Edit</span>
|
<span className="flex-1">Edit</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+E</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+E</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={contextMenuSplit} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={contextMenuSplit} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="scissors" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5 text-cyan-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Split</span>
|
<span className="flex-1">Split</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">S</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">S</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={contextMenuMerge} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={contextMenuMerge} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="combine" className="w-3.5 h-3.5 text-purple-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="combine" className="w-3.5 h-3.5 text-purple-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Merge</span>
|
<span className="flex-1">Merge</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+M</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+M</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-zinc-700 my-1"></div>
|
<div className="h-px bg-zinc-700 my-1"></div>
|
||||||
<button onClick={contextMenuCopy} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={contextMenuCopy} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="copy" className="w-3.5 h-3.5 text-zinc-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Copy</span>
|
<span className="flex-1">Copy</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+C</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+C</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={contextMenuCut} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={contextMenuCut} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="scissors" className="w-3.5 h-3.5 text-rose-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Cut</span>
|
<span className="flex-1">Cut</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+X</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+X</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={contextMenuPaste} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
<button onClick={contextMenuPaste} className="w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2">
|
||||||
<i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="clipboard" className="w-3.5 h-3.5 text-emerald-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Paste</span>
|
<span className="flex-1">Paste</span>
|
||||||
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+V</span>
|
<span className="text-purple-400 text-[12px] font-semibold font-mono ml-auto pl-8">Ctrl+V</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-zinc-700 my-1"></div>
|
<div className="h-px bg-zinc-700 my-1"></div>
|
||||||
<button onClick={contextMenuDelete} className="w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2">
|
<button onClick={contextMenuDelete} className="w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2">
|
||||||
<i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i>
|
<span className="inline-flex items-center shrink-0"><i data-lucide="trash-2" className="w-3.5 h-3.5 text-red-400 shrink-0"></i></span>
|
||||||
<span className="flex-1">Delete</span>
|
<span className="flex-1">Delete</span>
|
||||||
<span className="text-amber-400 text-[12px] font-semibold font-mono ml-auto pl-8">Del</span>
|
<span className="text-amber-400 text-[12px] font-semibold font-mono ml-auto pl-8">Del</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -6781,11 +6811,11 @@
|
|||||||
{/* ── Toast ── */}
|
{/* ── Toast ── */}
|
||||||
{toastMessage && (
|
{toastMessage && (
|
||||||
<div className="absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800">
|
<div className="absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800">
|
||||||
<i data-lucide="bell" className={`w-4 h-4 ${
|
<span className="inline-flex items-center shrink-0"><i data-lucide="bell" className={`w-4 h-4 ${
|
||||||
toastMessage.type === 'success' ? 'text-emerald-400' :
|
toastMessage.type === 'success' ? 'text-emerald-400' :
|
||||||
toastMessage.type === 'error' ? 'text-rose-400' :
|
toastMessage.type === 'error' ? 'text-rose-400' :
|
||||||
toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'
|
toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'
|
||||||
}`}></i>
|
}`}></i></span>
|
||||||
{toastMessage.text}
|
{toastMessage.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user