feat: unified search panel — progressive keyword+semantic results (#80)
* feat: unify search panel — remove keyword/semantic toggle, progressive results Replace separate keyword/semantic mode toggle with a unified search that streams results progressively: keyword results appear first (<500ms), then hybrid results augment the list (~1-2s). Loading spinner shows in the input field while semantic search runs. Stale requests are discarded via generation counter for safe rapid typing. - Extract search logic into useUnifiedSearch hook (code health 9.26+) - 300ms debounce, 5s timeout on hybrid, graceful fallback on errors - 15 tests covering progressive search, spinner, stale results, failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add design wireframes for unified search panel Two frames showing the search panel states: - "keyword results loaded, semantic loading" (with spinner) - "all results loaded" (no spinner, more results including semantic) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: coverage artifacts --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,30 +23,30 @@
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">51.85% </span>
|
||||
<span class="strong">94.82% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>28/54</span>
|
||||
<span class='fraction'>55/58</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">26.31% </span>
|
||||
<span class="strong">85.36% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>10/38</span>
|
||||
<span class='fraction'>35/41</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">54.54% </span>
|
||||
<span class="strong">93.33% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>6/11</span>
|
||||
<span class='fraction'>14/15</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">56.25% </span>
|
||||
<span class="strong">95.74% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>27/48</span>
|
||||
<span class='fraction'>45/47</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line medium'></div>
|
||||
<div class='status-line high'></div>
|
||||
<pre><table class="coverage">
|
||||
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||
<a name='L2'></a><a href='#L2'>2</a>
|
||||
@@ -186,10 +186,7 @@
|
||||
<a name='L121'></a><a href='#L121'>121</a>
|
||||
<a name='L122'></a><a href='#L122'>122</a>
|
||||
<a name='L123'></a><a href='#L123'>123</a>
|
||||
<a name='L124'></a><a href='#L124'>124</a>
|
||||
<a name='L125'></a><a href='#L125'>125</a>
|
||||
<a name='L126'></a><a href='#L126'>126</a>
|
||||
<a name='L127'></a><a href='#L127'>127</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<a name='L124'></a><a href='#L124'>124</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -215,11 +212,11 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">21x</span>
|
||||
<span class="cline-any cline-yes">39x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">21x</span>
|
||||
<span class="cline-any cline-yes">39x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -228,13 +225,13 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-yes">4x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">6x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
@@ -244,80 +241,77 @@
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">6x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">21x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">22x</span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-yes">2x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-yes">1x</span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-no"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">12x</span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">8x</span>
|
||||
<span class="cline-any cline-yes">5x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">264x</span>
|
||||
<span class="cline-any cline-yes">264x</span>
|
||||
<span class="cline-any cline-yes">264x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">39x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">44x</span>
|
||||
<span class="cline-any cline-yes">19x</span>
|
||||
<span class="cline-any cline-yes">19x</span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">13x</span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">7x</span>
|
||||
<span class="cline-any cline-yes">3x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-yes">19x</span>
|
||||
<span class="cline-any cline-yes">19x</span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span>
|
||||
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useMemo, useRef } from 'react'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
import { filterEntries, sortByModified, buildRelationshipGroups } from '../components/NoteList'
|
||||
import { filterEntries, sortByModified, buildRelationshipGroups } from '../utils/noteListHelpers'
|
||||
import type { VaultEntry, SidebarSelection } from '../types'
|
||||
|
||||
interface Tab {
|
||||
@@ -348,98 +342,95 @@ function computeVisibleNotes(
|
||||
return [...filterEntries(entries, selection)].sort(sortByModified)
|
||||
}
|
||||
|
||||
function <span class="fstat-no" title="function not covered" >navigateTab(</span>
|
||||
function navigateTab(
|
||||
tabsRef: React.RefObject<Tab[]>,
|
||||
activeTabPathRef: React.RefObject<string | null>,
|
||||
onSwitchTab: React.RefObject<(path: string) => void>,
|
||||
direction: 1 | -1,
|
||||
) {
|
||||
const currentTabs = <span class="cstat-no" title="statement not covered" >tabsRef.current!</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (currentTabs.length === 0) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||
const currentTabs = tabsRef.current!
|
||||
if (currentTabs.length === 0) return
|
||||
|
||||
const currentPath = <span class="cstat-no" title="statement not covered" >activeTabPathRef.current</span>
|
||||
const currentIndex = <span class="cstat-no" title="statement not covered" >currentTabs.findIndex(<span class="fstat-no" title="function not covered" >(t</span>) => <span class="cstat-no" title="statement not covered" >t.entry.path === currentPath)</span></span>
|
||||
const <span class="cstat-no" title="statement not covered" >nextIndex = (currentIndex + direction + currentTabs.length) % currentTabs.length</span>
|
||||
<span class="cstat-no" title="statement not covered" > onSwitchTab.current!(currentTabs[nextIndex].entry.path)</span>
|
||||
const currentPath = activeTabPathRef.current
|
||||
const currentIndex = currentTabs.findIndex((t) => t.entry.path === currentPath)
|
||||
const nextIndex = (currentIndex + direction + currentTabs.length) % currentTabs.length
|
||||
onSwitchTab.current!(currentTabs[nextIndex].entry.path)
|
||||
}
|
||||
|
||||
function <span class="fstat-no" title="function not covered" >navigateNote(</span>
|
||||
function navigateNote(
|
||||
visibleNotesRef: React.RefObject<VaultEntry[]>,
|
||||
activeTabPathRef: React.RefObject<string | null>,
|
||||
onReplace: React.RefObject<(entry: VaultEntry) => void>,
|
||||
onSelect: React.RefObject<(entry: VaultEntry) => void>,
|
||||
direction: 1 | -1,
|
||||
) {
|
||||
const notes = <span class="cstat-no" title="statement not covered" >visibleNotesRef.current!</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (notes.length === 0) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||
const notes = visibleNotesRef.current!
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (notes.length === 0) <span class="cstat-no" title="statement not covered" >return</span>
|
||||
|
||||
const currentPath = <span class="cstat-no" title="statement not covered" >activeTabPathRef.current</span>
|
||||
const currentIndex = <span class="cstat-no" title="statement not covered" >notes.findIndex(<span class="fstat-no" title="function not covered" >(n</span>) => <span class="cstat-no" title="statement not covered" >n.path === currentPath)</span></span>
|
||||
const currentPath = activeTabPathRef.current
|
||||
const currentIndex = notes.findIndex((n) => n.path === currentPath)
|
||||
|
||||
const nextIndex = <span class="cstat-no" title="statement not covered" >currentIndex === -1</span>
|
||||
? (direction === 1 ? 0 : notes.length - 1)
|
||||
const nextIndex = currentIndex === -1
|
||||
? (direction === 1 ? 0 : <span class="branch-1 cbranch-no" title="branch not covered" >notes.length - 1)</span>
|
||||
: (currentIndex + direction + notes.length) % notes.length
|
||||
|
||||
const nextNote = <span class="cstat-no" title="statement not covered" >notes[nextIndex]</span>
|
||||
<span class="cstat-no" title="statement not covered" > if (currentPath) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > onReplace.current!(nextNote)</span>
|
||||
const nextNote = notes[nextIndex]
|
||||
if (currentPath) {
|
||||
onReplace.current!(nextNote)
|
||||
} else {
|
||||
<span class="cstat-no" title="statement not covered" > onSelect.current!(nextNote)</span>
|
||||
onSelect.current!(nextNote)
|
||||
}
|
||||
}
|
||||
|
||||
type ShortcutKind = 'tab' | 'note' | null
|
||||
|
||||
function classifyShortcut(e: KeyboardEvent, inTauri: boolean): ShortcutKind {
|
||||
const mod = e.metaKey || e.ctrlKey
|
||||
if (!mod) return null
|
||||
const isTabShortcut = inTauri ? (<span class="branch-0 cbranch-no" title="branch not covered" >e.altKey && !e.shiftKey) : (e</span>.shiftKey && !e.altKey)
|
||||
if (isTabShortcut && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return 'tab'
|
||||
if (e.altKey && !e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) return 'note'
|
||||
return null
|
||||
}
|
||||
|
||||
function arrowDirection(key: string): 1 | -1 {
|
||||
return (key === 'ArrowRight' || key === 'ArrowDown') ? 1 : -1
|
||||
}
|
||||
|
||||
function useLatestRef<T>(value: T): React.RefObject<T> {
|
||||
const ref = useRef(value)
|
||||
useEffect(() => { ref.current = value })
|
||||
return ref
|
||||
}
|
||||
|
||||
export function useKeyboardNavigation({
|
||||
tabs,
|
||||
activeTabPath,
|
||||
entries,
|
||||
selection,
|
||||
allContent,
|
||||
onSwitchTab,
|
||||
onReplaceActiveTab,
|
||||
onSelectNote,
|
||||
tabs, activeTabPath, entries, selection, allContent,
|
||||
onSwitchTab, onReplaceActiveTab, onSelectNote,
|
||||
}: KeyboardNavigationOptions) {
|
||||
const visibleNotes = useMemo(
|
||||
() => computeVisibleNotes(entries, selection, allContent),
|
||||
[entries, selection, allContent],
|
||||
)
|
||||
|
||||
const tabsRef = useRef(tabs)
|
||||
tabsRef.current = tabs
|
||||
const activeTabPathRef = useRef(activeTabPath)
|
||||
activeTabPathRef.current = activeTabPath
|
||||
const visibleNotesRef = useRef(visibleNotes)
|
||||
visibleNotesRef.current = visibleNotes
|
||||
const onSwitchTabRef = useRef(onSwitchTab)
|
||||
onSwitchTabRef.current = onSwitchTab
|
||||
const onReplaceRef = useRef(onReplaceActiveTab)
|
||||
onReplaceRef.current = onReplaceActiveTab
|
||||
const onSelectNoteRef = useRef(onSelectNote)
|
||||
onSelectNoteRef.current = onSelectNote
|
||||
const tabsRef = useLatestRef(tabs)
|
||||
const activeTabPathRef = useLatestRef(activeTabPath)
|
||||
const visibleNotesRef = useLatestRef(visibleNotes)
|
||||
const onSwitchTabRef = useLatestRef(onSwitchTab)
|
||||
const onReplaceRef = useLatestRef(onReplaceActiveTab)
|
||||
const onSelectNoteRef = useLatestRef(onSelectNote)
|
||||
|
||||
useEffect(() => {
|
||||
const isRunningInTauri = isTauri()
|
||||
|
||||
const inTauri = isTauri()
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const mod = e.metaKey || <span class="branch-1 cbranch-no" title="branch not covered" >e.ctrlKey</span>
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (!mod) <span class="cstat-no" title="statement not covered" >return</span>
|
||||
|
||||
const isTabShortcut = isRunningInTauri
|
||||
? <span class="branch-0 cbranch-no" title="branch not covered" >e.altKey && !e.shiftKey</span>
|
||||
: e.shiftKey && <span class="branch-1 cbranch-no" title="branch not covered" >!e.altKey</span>
|
||||
const isNoteShortcut = e.altKey && <span class="branch-1 cbranch-no" title="branch not covered" >!e.shiftKey</span>
|
||||
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>if (isTabShortcut && (<span class="branch-1 cbranch-no" title="branch not covered" >e.key === 'ArrowLeft' || <span class="branch-2 cbranch-no" title="branch not covered" >e</span>.key === 'ArrowRight')) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > e.preventDefault()</span>
|
||||
<span class="cstat-no" title="statement not covered" > navigateTab(tabsRef, activeTabPathRef, onSwitchTabRef, e.key === 'ArrowRight' ? 1 : -1)</span>
|
||||
<span class="missing-if-branch" title="if path not taken" >I</span>} else if (isNoteShortcut && (<span class="branch-1 cbranch-no" title="branch not covered" >e.key === 'ArrowUp' || <span class="branch-2 cbranch-no" title="branch not covered" >e</span>.key === 'ArrowDown')) {</span>
|
||||
<span class="cstat-no" title="statement not covered" > e.preventDefault()</span>
|
||||
<span class="cstat-no" title="statement not covered" > navigateNote(visibleNotesRef, activeTabPathRef, onReplaceRef, onSelectNoteRef, e.key === 'ArrowDown' ? 1 : -1)</span>
|
||||
}
|
||||
const kind = classifyShortcut(e, inTauri)
|
||||
if (!kind) return
|
||||
e.preventDefault()
|
||||
if (kind === 'tab') navigateTab(tabsRef, activeTabPathRef, onSwitchTabRef, arrowDirection(e.key))
|
||||
else navigateNote(visibleNotesRef, activeTabPathRef, onReplaceRef, onSelectNoteRef, arrowDirection(e.key))
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
}, [tabsRef, activeTabPathRef, visibleNotesRef, onSwitchTabRef, onReplaceRef, onSelectNoteRef])
|
||||
}
|
||||
</pre></td></tr></table></pre>
|
||||
|
||||
@@ -448,7 +439,7 @@ export function useKeyboardNavigation({
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2026-02-22T09:10:55.587Z
|
||||
at 2026-02-25T20:19:25.575Z
|
||||
</div>
|
||||
<script src="../../prettify.js"></script>
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user