|
1 | 1 | import { useState, useEffect, useCallback, useRef } from 'react'; |
2 | | -import { Trash2, Plus, RefreshCw, Terminal, CheckCircle2, Copy, Circle, X, AlertTriangle, Download, Upload, ArrowUp, ArrowDown, ChevronsUpDown } from 'lucide-react'; |
| 2 | +import { Trash2, Plus, RefreshCw, Terminal, CheckCircle2, Copy, Circle, X, AlertTriangle, Download, Upload, ArrowUp, ArrowDown, ChevronsUpDown, Pencil } from 'lucide-react'; |
3 | 3 | import { toast } from 'sonner'; |
4 | 4 | import { sounds } from '@/lib/sound'; |
5 | 5 | import { decryptKeys, maskKey } from '@/utils/crypto'; |
@@ -38,6 +38,27 @@ interface SortConfig { |
38 | 38 | } |
39 | 39 |
|
40 | 40 | const SORT_STORAGE_KEY = 'oroio-key-sort'; |
| 41 | +const NOTES_STORAGE_KEY = 'oroio-key-notes'; |
| 42 | + |
| 43 | +function loadNotes(): Record<string, string> { |
| 44 | + try { |
| 45 | + const saved = localStorage.getItem(NOTES_STORAGE_KEY); |
| 46 | + if (saved) return JSON.parse(saved); |
| 47 | + } catch { |
| 48 | + // ignore parse errors |
| 49 | + } |
| 50 | + return {}; |
| 51 | +} |
| 52 | + |
| 53 | +function saveNote(key: string, note: string) { |
| 54 | + const notes = loadNotes(); |
| 55 | + if (note.trim()) { |
| 56 | + notes[key] = note; |
| 57 | + } else { |
| 58 | + delete notes[key]; |
| 59 | + } |
| 60 | + localStorage.setItem(NOTES_STORAGE_KEY, JSON.stringify(notes)); |
| 61 | +} |
41 | 62 |
|
42 | 63 | function loadSortConfig(): SortConfig { |
43 | 64 | try { |
@@ -122,6 +143,61 @@ function KeyDisplay({ keyText, isCurrent, className }: { keyText: string, isCurr |
122 | 143 | ); |
123 | 144 | } |
124 | 145 |
|
| 146 | +function NoteCell({ keyText, onUpdate }: { keyText: string; onUpdate: () => void }) { |
| 147 | + const [editing, setEditing] = useState(false); |
| 148 | + const [note, setNote] = useState(() => loadNotes()[keyText] || ''); |
| 149 | + const inputRef = useRef<HTMLInputElement>(null); |
| 150 | + |
| 151 | + useEffect(() => { |
| 152 | + if (editing && inputRef.current) { |
| 153 | + inputRef.current.focus(); |
| 154 | + } |
| 155 | + }, [editing]); |
| 156 | + |
| 157 | + const handleSave = () => { |
| 158 | + saveNote(keyText, note); |
| 159 | + setEditing(false); |
| 160 | + onUpdate(); |
| 161 | + }; |
| 162 | + |
| 163 | + const handleKeyDown = (e: React.KeyboardEvent) => { |
| 164 | + if (e.key === 'Enter') { |
| 165 | + handleSave(); |
| 166 | + } else if (e.key === 'Escape') { |
| 167 | + setNote(loadNotes()[keyText] || ''); |
| 168 | + setEditing(false); |
| 169 | + } |
| 170 | + }; |
| 171 | + |
| 172 | + if (editing) { |
| 173 | + return ( |
| 174 | + <input |
| 175 | + ref={inputRef} |
| 176 | + type="text" |
| 177 | + value={note} |
| 178 | + onChange={(e) => setNote(e.target.value)} |
| 179 | + onBlur={handleSave} |
| 180 | + onKeyDown={handleKeyDown} |
| 181 | + className="w-full px-1.5 py-0.5 text-xs bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary" |
| 182 | + placeholder="Note..." |
| 183 | + /> |
| 184 | + ); |
| 185 | + } |
| 186 | + |
| 187 | + return ( |
| 188 | + <div |
| 189 | + className="flex items-center gap-1 group cursor-pointer min-h-[24px]" |
| 190 | + onClick={() => setEditing(true)} |
| 191 | + title="Click to edit" |
| 192 | + > |
| 193 | + <span className="text-xs text-muted-foreground"> |
| 194 | + {note || '-'} |
| 195 | + </span> |
| 196 | + <Pencil className="h-3 w-3 text-muted-foreground/40 opacity-0 group-hover:opacity-100 transition-opacity" /> |
| 197 | + </div> |
| 198 | + ); |
| 199 | +} |
| 200 | + |
125 | 201 | function IconCopyButton({ text, icon: Icon, title, className }: { text: string; icon: any; title: string; className?: string }) { |
126 | 202 | const [copied, setCopied] = useState(false); |
127 | 203 |
|
@@ -202,6 +278,7 @@ export default function KeyList() { |
202 | 278 | const [adding, setAdding] = useState(false); |
203 | 279 | const fileInputRef = useRef<HTMLInputElement>(null); |
204 | 280 | const [sortConfig, setSortConfig] = useState<SortConfig>(loadSortConfig); |
| 281 | + const [, setNotesVersion] = useState(0); |
205 | 282 |
|
206 | 283 | const handleSort = (field: SortField) => { |
207 | 284 | const newConfig: SortConfig = { |
@@ -474,18 +551,20 @@ export default function KeyList() { |
474 | 551 | <Table className="table-fixed"> |
475 | 552 | <colgroup> |
476 | 553 | <col style={{ width: '4%' }} /> |
477 | | - <col style={{ width: '5%' }} /> |
478 | | - <col style={{ width: '22%' }} /> |
479 | | - <col style={{ width: '8%' }} /> |
480 | | - <col style={{ width: '18%' }} /> |
481 | | - <col style={{ width: '18%' }} /> |
| 554 | + <col style={{ width: '4%' }} /> |
| 555 | + <col style={{ width: '17%' }} /> |
| 556 | + <col style={{ width: '17%' }} /> |
| 557 | + <col style={{ width: '7%' }} /> |
| 558 | + <col style={{ width: '15%' }} /> |
| 559 | + <col style={{ width: '15%' }} /> |
482 | 560 | <col style={{ width: '10%' }} /> |
483 | 561 | </colgroup> |
484 | 562 | <TableHeader> |
485 | 563 | <TableRow className="bg-muted/30 hover:bg-muted/30"> |
486 | 564 | <TableHead></TableHead> |
487 | 565 | <TableHead className="text-xs tracking-wider">NO</TableHead> |
488 | 566 | <TableHead className="text-xs tracking-wider">KEY</TableHead> |
| 567 | + <TableHead className="text-xs tracking-wider">NOTE</TableHead> |
489 | 568 | <SortableHeader field="percent" label="%" align="right" sortConfig={sortConfig} onSort={handleSort} /> |
490 | 569 | <SortableHeader field="quota" label="QUOTA" align="right" sortConfig={sortConfig} onSort={handleSort} /> |
491 | 570 | <SortableHeader field="expiry" label="EXPIRY" align="center" sortConfig={sortConfig} onSort={handleSort} /> |
@@ -537,6 +616,9 @@ export default function KeyList() { |
537 | 616 | isCurrent={info.isCurrent} |
538 | 617 | /> |
539 | 618 | </TableCell> |
| 619 | + <TableCell className="py-2"> |
| 620 | + <NoteCell keyText={info.key} onUpdate={() => setNotesVersion(v => v + 1)} /> |
| 621 | + </TableCell> |
540 | 622 | <TableCell className="text-right font-mono text-sm text-muted-foreground py-2"> |
541 | 623 | {info.usage?.total ? `${percent}%` : '-'} |
542 | 624 | </TableCell> |
|
0 commit comments