202 lines
4.2 KiB
Vue
202 lines
4.2 KiB
Vue
<template>
|
||
<div
|
||
v-if="fileContent"
|
||
class="content-viewer"
|
||
:class="{
|
||
'dark-mode': preferences.darkMode,
|
||
'line-wrap': preferences.lineWrap
|
||
}"
|
||
>
|
||
<RecycleScroller
|
||
v-if="displayLines.length > 1000"
|
||
ref="virtualScroller"
|
||
class="scroller"
|
||
:items="displayLines"
|
||
:item-size="lineHeight"
|
||
key-field="lineNumber"
|
||
>
|
||
<template #default="{ item }">
|
||
<div
|
||
:data-line="item.lineNumber"
|
||
:class="[
|
||
'line',
|
||
{ selected: selectedLines.has(item.lineNumber) },
|
||
{ 'highlight-match': item.hasMatch },
|
||
{ 'current-result': isCurrentResult(item.lineNumber) }
|
||
]"
|
||
@click="toggleLineSelection(item.lineNumber, $event)"
|
||
>
|
||
<span v-if="preferences.showLineNumbers" class="line-number">
|
||
{{ item.lineNumber }}
|
||
</span>
|
||
<pre
|
||
v-highlight="highlightConfig"
|
||
><code>{{ item.content }}</code></pre>
|
||
</div>
|
||
</template>
|
||
</RecycleScroller>
|
||
|
||
<div v-else class="lines-container">
|
||
<div
|
||
v-for="line in displayLines"
|
||
:key="line.lineNumber"
|
||
:data-line="line.lineNumber"
|
||
:class="[
|
||
'line',
|
||
{ selected: selectedLines.has(line.lineNumber) },
|
||
{ 'highlight-match': line.hasMatch },
|
||
{ 'current-result': isCurrentResult(line.lineNumber) }
|
||
]"
|
||
@click="toggleLineSelection(line.lineNumber, $event)"
|
||
>
|
||
<span v-if="preferences.showLineNumbers" class="line-number">
|
||
{{ line.lineNumber }}
|
||
</span>
|
||
<pre v-highlight="highlightConfig"><code>{{ line.content }}</code></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="fileTooLarge" class="warning-banner">
|
||
⚠️ File exceeds 10,000 lines. Showing first 10,000 lines only. Use search
|
||
or filters to find specific content.
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, toRef, ref } from 'vue';
|
||
import { RecycleScroller } from 'vue-virtual-scroller';
|
||
import { useLineSelection } from '../../composables/useLineSelection.js';
|
||
|
||
const props = defineProps({
|
||
displayLines: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
fileContent: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
selectedFile: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
fileTooLarge: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
preferences: {
|
||
type: Object,
|
||
default: () => ({
|
||
darkMode: false,
|
||
lineWrap: false,
|
||
showLineNumbers: true
|
||
})
|
||
},
|
||
searchResults: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
currentResultIndex: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
highlightConfig: {
|
||
type: Object,
|
||
default: () => ({
|
||
theme: 'github',
|
||
language: 'json'
|
||
})
|
||
},
|
||
lineHeight: {
|
||
type: Number,
|
||
default: 20
|
||
}
|
||
});
|
||
|
||
const displayLines = toRef(props, 'displayLines');
|
||
const fileContent = toRef(props, 'fileContent');
|
||
const selectedFile = toRef(props, 'selectedFile');
|
||
|
||
const virtualScroller = ref(null);
|
||
|
||
const { selectedLines, toggleLineSelection } = useLineSelection(
|
||
displayLines,
|
||
fileContent,
|
||
selectedFile
|
||
);
|
||
|
||
const isCurrentResult = lineNumber => {
|
||
if (!props.searchResults.length) return false;
|
||
const currentLine = props.searchResults[props.currentResultIndex] + 1;
|
||
return lineNumber === currentLine;
|
||
};
|
||
|
||
defineExpose({
|
||
selectedLines,
|
||
toggleLineSelection,
|
||
virtualScroller
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.content-viewer {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
background: #fafafa;
|
||
max-height: 70vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.content-viewer.dark-mode {
|
||
background: #1e1e1e;
|
||
border-color: #333;
|
||
color: #eee;
|
||
}
|
||
|
||
.content-viewer.line-wrap pre {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.scroller,
|
||
.lines-container {
|
||
max-height: 65vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.line {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 2px 8px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.line.selected {
|
||
background: rgba(0, 123, 255, 0.15);
|
||
}
|
||
|
||
.line.highlight-match {
|
||
background: rgba(255, 234, 0, 0.2);
|
||
}
|
||
|
||
.line.current-result {
|
||
outline: 1px solid #ff9800;
|
||
}
|
||
|
||
.line-number {
|
||
width: 50px;
|
||
text-align: right;
|
||
color: #999;
|
||
user-select: none;
|
||
}
|
||
|
||
.warning-banner {
|
||
padding: 8px 10px;
|
||
font-size: 13px;
|
||
background: #fff3cd;
|
||
border-top: 1px solid #ffeeba;
|
||
color: #856404;
|
||
}
|
||
</style>
|