mirror of
https://github.com/xlmnxp/extractify.zip.git
synced 2024-11-24 01:24:35 +03:00
Compare commits
7 Commits
c8209c5799
...
3059a2f252
Author | SHA1 | Date | |
---|---|---|---|
3059a2f252 | |||
8b1b7d7aa2 | |||
249cc552ec | |||
dac3982370 | |||
21dc8009ba | |||
816f50a452 | |||
5a739b543d |
121
app.vue
121
app.vue
@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getElementInfo } from "moveable";
|
|
||||||
import { VueSelecto } from "vue3-selecto";
|
|
||||||
import { useDisplay } from 'vuetify/lib/framework.mjs';
|
import { useDisplay } from 'vuetify/lib/framework.mjs';
|
||||||
import { HistoryManager } from './composables/history-manager';
|
import { HistoryManager } from './composables/history-manager';
|
||||||
import { FilesManager } from './composables/files-manager';
|
import { FilesManager, supportedExtensions, imageExtensions } from './composables/files-manager';
|
||||||
import type { iFile } from "composables/worker/7zip-manager"
|
import type { iFile } from "composables/worker/7zip-manager"
|
||||||
|
import { videoExtensions, binaryExtensions } from '#imports';
|
||||||
|
|
||||||
let display = useDisplay();
|
let display = useDisplay();
|
||||||
let drawer = ref(!display.mdAndDown.value);
|
let drawer = ref(!display.mdAndDown.value);
|
||||||
@ -18,13 +17,21 @@ let selectedList = ref<any>([]);
|
|||||||
let filesManager = new FilesManager(filesList);
|
let filesManager = new FilesManager(filesList);
|
||||||
let history = new HistoryManager(filesManager);
|
let history = new HistoryManager(filesManager);
|
||||||
let mediaBlobUrl = ref('');
|
let mediaBlobUrl = ref('');
|
||||||
|
let errorDialog = ref(false);
|
||||||
|
let errorMessage = ref('');
|
||||||
|
|
||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
if (files.value?.[0]) {
|
if (files.value?.[0]) {
|
||||||
loadingModel.value = true;
|
loadingModel.value = true;
|
||||||
filesList.value = [];
|
filesList.value = [];
|
||||||
|
|
||||||
|
try {
|
||||||
await filesManager.loadArchive(files.value?.[0]);
|
await filesManager.loadArchive(files.value?.[0]);
|
||||||
|
} catch (error: any) {
|
||||||
|
errorMessage.value = error.message;
|
||||||
|
errorDialog.value = true;
|
||||||
|
files.value = [];
|
||||||
|
}
|
||||||
loadingModel.value = false;
|
loadingModel.value = false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -57,41 +64,13 @@ watchEffect(async () => {
|
|||||||
const file = filesManager.getFile(selectedPath.value);
|
const file = filesManager.getFile(selectedPath.value);
|
||||||
filesGridList.value = file?.content;
|
filesGridList.value = file?.content;
|
||||||
|
|
||||||
selectedList.value = [];
|
// Update to handle both video and image files
|
||||||
for (const selectedElement of document.querySelectorAll(".selectable.selected")) {
|
if (videoExtensions.includes(filesManager.getFile(selectedPath.value)?.extension?.toLowerCase()) ||
|
||||||
selectedElement.classList.remove("selected");
|
imageExtensions.includes(filesManager.getFile(selectedPath.value)?.extension?.toLowerCase())) {
|
||||||
}
|
|
||||||
|
|
||||||
// Experimental feature
|
|
||||||
if (videoExtensions.includes(filesManager.getFile(selectedPath.value)?.extension?.toLowerCase())) {
|
|
||||||
mediaBlobUrl.value = await filesManager.getFileBlobUrl(selectedPath.value) as string;
|
mediaBlobUrl.value = await filesManager.getFileBlobUrl(selectedPath.value) as string;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const dragContainer = document.querySelector(".select-area");
|
|
||||||
|
|
||||||
function onSelectStart(e: any) {
|
|
||||||
e.added.forEach((el: any) => {
|
|
||||||
el.classList.add("selected");
|
|
||||||
selectedList.value = [...selectedList.value, el?.__vnode?.ctx?.props?.value];
|
|
||||||
});
|
|
||||||
e.removed.forEach((el: any) => {
|
|
||||||
el.classList.remove("selected");
|
|
||||||
selectedList.value = selectedList.value.filter((value: string) => value != el?.__vnode?.ctx?.props?.value)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectEnd(e: any) {
|
|
||||||
e.afterAdded.forEach((el: any) => {
|
|
||||||
el.classList.add("selected");
|
|
||||||
selectedList.value = [...selectedList.value, el?.__vnode?.ctx?.props?.value];
|
|
||||||
});
|
|
||||||
e.afterRemoved.forEach((el: any) => {
|
|
||||||
el.classList.remove("selected");
|
|
||||||
selectedList.value = selectedList.value.filter((value: string) => value != el?.__vnode?.ctx?.props?.value)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// step up from current path
|
// step up from current path
|
||||||
function stepUp(path: string) {
|
function stepUp(path: string) {
|
||||||
const pathArray = path.split("/");
|
const pathArray = path.split("/");
|
||||||
@ -123,9 +102,8 @@ function stepUp(path: string) {
|
|||||||
</div>
|
</div>
|
||||||
</v-footer>
|
</v-footer>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<v-main class="select-area" style="height: 100dvh;">
|
<v-main style="height: 100dvh;">
|
||||||
<v-toolbar class="px-5" height="auto">
|
<v-toolbar class="px-5" height="auto">
|
||||||
|
|
||||||
<v-row align="center" justify="center">
|
<v-row align="center" justify="center">
|
||||||
<v-col cols="12" lg="2" md="12" style="display: inline-flex;">
|
<v-col cols="12" lg="2" md="12" style="display: inline-flex;">
|
||||||
<v-btn title="Back" aria-label="Back" icon="mdi-arrow-left" :disabled="!history.hasUndo.value"
|
<v-btn title="Back" aria-label="Back" icon="mdi-arrow-left" :disabled="!history.hasUndo.value"
|
||||||
@ -161,44 +139,27 @@ function stepUp(path: string) {
|
|||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-container>
|
<v-container>
|
||||||
<template v-if="filesManager.getFile(selectedPath)?.isFolder">
|
<template v-if="filesManager.getFile(selectedPath)?.isFolder">
|
||||||
<v-list :selected="[selectedPath]">
|
<FolderViewer :filesManager="filesManager" :filesGridList="filesGridList" :selectedList="selectedList"></FolderViewer>
|
||||||
<v-row no-gutters>
|
|
||||||
<v-col cols="6" lg="2" md="3" sm="6" v-for="file of filesGridList" style="text-align: center;">
|
|
||||||
<v-list-item class="position-relative ma-2 pa-5 selectable" active-color="light-blue-darken-4"
|
|
||||||
:value="file.path" rounded @click="selectedPath = file.path">
|
|
||||||
<v-menu v-if="!file.isFolder">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn class="position-absolute" style="right: 0; top: 0;" icon="mdi-dots-vertical" variant="text"
|
|
||||||
v-bind="props"></v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item title="Download" aria-label="Download" icon="mdi-download"
|
|
||||||
@click="filesManager.downloadFile(file.path)">
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon icon="mdi-download"></v-icon>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<file-logo class="mb-2" :file="file" :key="file.path" />
|
|
||||||
<p>{{ file.name }}</p>
|
|
||||||
</v-list-item>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-list>
|
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-if="!filesManager.getFile(selectedPath)?.isFolder && videoExtensions.includes(filesManager.getFile(selectedPath)?.extension)">
|
v-if="!filesManager.getFile(selectedPath)?.isFolder && videoExtensions.includes(filesManager.getFile(selectedPath)?.extension)">
|
||||||
<MediaVideoPlayer :src="mediaBlobUrl"></MediaVideoPlayer>
|
<MediaVideoPlayer :src="mediaBlobUrl"></MediaVideoPlayer>
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-if="!filesManager.getFile(selectedPath)?.isFolder && files.length && !videoExtensions.includes(filesManager.getFile(selectedPath)?.extension)">
|
v-if="!filesManager.getFile(selectedPath)?.isFolder && imageExtensions.includes(filesManager.getFile(selectedPath)?.extension)">
|
||||||
<TextEditor :file="filesManager.getFile(selectedPath)" :filesManager="filesManager"></TextEditor>
|
<MediaImageViewer :src="mediaBlobUrl"></MediaImageViewer>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="!filesManager.getFile(selectedPath)?.isFolder && files.length && !imageExtensions.includes(filesManager.getFile(selectedPath)?.extension) && !videoExtensions.includes(filesManager.getFile(selectedPath)?.extension) && !binaryExtensions.includes(filesManager.getFile(selectedPath)?.extension)">
|
||||||
|
<MediaTextEditor :file="filesManager.getFile(selectedPath)" :filesManager="filesManager"></MediaTextEditor>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="!filesManager.getFile(selectedPath)?.isFolder && files.length && binaryExtensions.includes(filesManager.getFile(selectedPath)?.extension)">
|
||||||
|
<MediaBinaryViewer :file="filesManager.getFile(selectedPath)" :filesManager="filesManager"></MediaBinaryViewer>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!files.length">
|
<template v-if="!files.length">
|
||||||
<!-- tutorial drag and drop zipped file here and review it securely -->
|
<!-- tutorial drag and drop zipped file here and review it securely -->
|
||||||
<v-row align="center" justify="center">
|
<v-row align="center" justify="center" style="height: calc(100vh - 120px)">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card variant="flat" class="mx-auto" max-width="768">
|
<v-card variant="flat" class="mx-auto" max-width="768">
|
||||||
<!-- v-icon for file -->
|
<!-- v-icon for file -->
|
||||||
@ -214,17 +175,14 @@ function stepUp(path: string) {
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<!-- file input -->
|
<!-- file input -->
|
||||||
<v-file-input class="mx-5" v-model="files" accept=".zip,.7z,.rar,.tar.bz2,.tar.gz,.tar.xz"
|
<v-file-input class="mx-5" v-model="files"
|
||||||
label="or select a file..." variant="outlined"></v-file-input>
|
:accept="supportedExtensions.map(extension => `.${extension}`).join(',')" label="or select a file..."
|
||||||
|
variant="outlined"></v-file-input>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</template>
|
</template>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<VueSelecto :selectableTargets="['.selectable']" :dragContainer="dragContainer" :hitRate="0"
|
|
||||||
:selectFromInside="false" :toggleContinueSelect="'ctrl'" @select="onSelectStart" @selectStart="onSelectStart"
|
|
||||||
:get-element-rect="getElementInfo" @selectEnd="onSelectEnd" :select-by-click="false" />
|
|
||||||
</v-main>
|
</v-main>
|
||||||
<v-dialog v-model="loadingModel" persistent width="auto">
|
<v-dialog v-model="loadingModel" persistent width="auto">
|
||||||
<v-card>
|
<v-card>
|
||||||
@ -234,6 +192,22 @@ function stepUp(path: string) {
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
<v-dialog v-model="errorDialog" width="auto">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-error">
|
||||||
|
Error
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="text" @click="errorDialog = false">
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style>
|
||||||
@ -244,4 +218,9 @@ function stepUp(path: string) {
|
|||||||
.selected {
|
.selected {
|
||||||
background: rgba(48, 150, 243, 0.1);
|
background: rgba(48, 150, 243, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-container {
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
79
components/folder-viewer.vue
Normal file
79
components/folder-viewer.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { getElementInfo } from "moveable";
|
||||||
|
import { VueSelecto } from "vue3-selecto";
|
||||||
|
import { FilesManager } from '~/composables/files-manager';
|
||||||
|
import { iFile } from '~/composables/worker/7zip-manager';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filesManager: FilesManager;
|
||||||
|
filesGridList: iFile[];
|
||||||
|
selectedList: Ref<string[]>
|
||||||
|
};
|
||||||
|
|
||||||
|
const { filesManager, filesGridList, selectedList } = defineProps<Props>();
|
||||||
|
|
||||||
|
const selectedPath = useSelectedPath();
|
||||||
|
|
||||||
|
const dragContainer = document.querySelector(".select-area");
|
||||||
|
|
||||||
|
function onSelectStart(e: any) {
|
||||||
|
e.added.forEach((el: any) => {
|
||||||
|
el.classList.add("selected");
|
||||||
|
selectedList.value = [...selectedList.value, el?.__vnode?.ctx?.props?.value];
|
||||||
|
});
|
||||||
|
e.removed.forEach((el: any) => {
|
||||||
|
el.classList.remove("selected");
|
||||||
|
selectedList.value = selectedList.value.filter((value: string) => value != el?.__vnode?.ctx?.props?.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectEnd(e: any) {
|
||||||
|
e.afterAdded.forEach((el: any) => {
|
||||||
|
el.classList.add("selected");
|
||||||
|
selectedList.value = [...selectedList.value, el?.__vnode?.ctx?.props?.value];
|
||||||
|
});
|
||||||
|
e.afterRemoved.forEach((el: any) => {
|
||||||
|
el.classList.remove("selected");
|
||||||
|
selectedList.value = selectedList.value.filter((value: string) => value != el?.__vnode?.ctx?.props?.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
selectedList.value = [];
|
||||||
|
for (const selectedElement of document.querySelectorAll(".selectable.selected")) {
|
||||||
|
selectedElement.classList.remove("selected");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-list class="select-area" :selected="[selectedPath]" style="height: calc(100vh - 120px);">
|
||||||
|
<v-row no-gutters>
|
||||||
|
<v-col cols="6" lg="2" md="3" sm="6" v-for="file of filesGridList" style="text-align: center;">
|
||||||
|
<v-list-item class="position-relative ma-2 pa-5 selectable" active-color="light-blue-darken-4"
|
||||||
|
:value="file.path" rounded @click="selectedPath = file.path">
|
||||||
|
<v-menu v-if="!file.isFolder">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn class="position-absolute" style="right: 0; top: 0;" icon="mdi-dots-vertical"
|
||||||
|
variant="text" v-bind="props"></v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item title="Download" aria-label="Download" icon="mdi-download"
|
||||||
|
@click="filesManager.downloadFile(file.path)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon icon="mdi-download"></v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<file-logo class="mb-2" :file="file" :key="file.path" />
|
||||||
|
<p>{{ file.name }}</p>
|
||||||
|
</v-list-item>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<VueSelecto :selectableTargets="['.selectable']" :dragContainer="dragContainer" :hitRate="0"
|
||||||
|
:selectFromInside="false" :toggleContinueSelect="'ctrl'" @select="onSelectStart" @selectStart="onSelectStart"
|
||||||
|
:get-element-rect="getElementInfo" @selectEnd="onSelectEnd" :select-by-click="false" />
|
||||||
|
</v-list>
|
||||||
|
</template>
|
166
components/media/binary-viewer.vue
Normal file
166
components/media/binary-viewer.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { FilesManager } from '~/composables/files-manager';
|
||||||
|
import { iFile } from '~/composables/worker/7zip-manager';
|
||||||
|
import { VBtn } from 'vuetify/components';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
file: iFile,
|
||||||
|
filesManager: FilesManager
|
||||||
|
}
|
||||||
|
|
||||||
|
const { file, filesManager } = defineProps<Props>()
|
||||||
|
const buffer = ref<Uint8Array>()
|
||||||
|
const containerRef = ref<HTMLElement>()
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const rowHeight = 24 // pixels per row
|
||||||
|
const visibleRows = ref(0)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
|
||||||
|
// Calculate total rows and visible window
|
||||||
|
const totalRows = computed(() => buffer.value ? Math.ceil(buffer.value.length / 16) : 0)
|
||||||
|
const startRow = computed(() => Math.floor(scrollTop.value / rowHeight))
|
||||||
|
const endRow = computed(() => Math.min(startRow.value + visibleRows.value + 50, totalRows.value))
|
||||||
|
|
||||||
|
// Get only the visible rows data
|
||||||
|
const visibleData = computed(() => {
|
||||||
|
if (!buffer.value) return []
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
for (let i = startRow.value; i < endRow.value; i++) {
|
||||||
|
const offset = i * 16
|
||||||
|
const chunk = buffer.value.slice(offset, offset + 16)
|
||||||
|
const hex = Array.from(chunk).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
||||||
|
const ascii = Array.from(chunk).map(b => b >= 32 && b <= 126 ? String.fromCharCode(b) : '.').join('')
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
address: offset.toString(16).padStart(8, '0'),
|
||||||
|
hex: hex.padEnd(48, ' '),
|
||||||
|
ascii
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle scroll events
|
||||||
|
function onScroll(event: Event) {
|
||||||
|
const container = event.target as HTMLElement
|
||||||
|
scrollTop.value = container.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate visible rows based on container height
|
||||||
|
function updateVisibleRows() {
|
||||||
|
if (containerRef.value) {
|
||||||
|
visibleRows.value = Math.ceil(containerRef.value.clientHeight / rowHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBinaryFile() {
|
||||||
|
filesManager.downloadFile(file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
buffer.value = await filesManager.getFileContent(file.path, "binary") as Uint8Array
|
||||||
|
updateVisibleRows()
|
||||||
|
window.addEventListener('resize', updateVisibleRows)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="binary-viewer">
|
||||||
|
<div class="toolbar">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
prepend-icon="mdi-download"
|
||||||
|
@click="downloadBinaryFile"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Download {{ file.name }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="loading">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
ref="containerRef"
|
||||||
|
class="hex-container"
|
||||||
|
@scroll="onScroll"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="scroll-content"
|
||||||
|
:style="{
|
||||||
|
height: `${totalRows * rowHeight}px`,
|
||||||
|
paddingTop: `${startRow * rowHeight}px`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="row in visibleData"
|
||||||
|
:key="row.address"
|
||||||
|
class="hex-line"
|
||||||
|
>
|
||||||
|
<span class="address">{{ row.address }}</span>
|
||||||
|
<span class="hex">{{ row.hex }}</span>
|
||||||
|
<span class="ascii">{{ row.ascii }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.binary-viewer {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--v-theme-surface);
|
||||||
|
height: calc(100vh - 148px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-content {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-line {
|
||||||
|
line-height: 1.5;
|
||||||
|
height: v-bind(rowHeight + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
|
.address {
|
||||||
|
color: #666;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
71
components/media/image-viewer.vue
Normal file
71
components/media/image-viewer.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps<{
|
||||||
|
src: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const scale = ref(1);
|
||||||
|
const rotation = ref(0);
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
scale.value = Math.min(scale.value + 0.1, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
scale.value = Math.max(scale.value - 0.1, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
scale.value = 1;
|
||||||
|
rotation.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateImage() {
|
||||||
|
rotation.value = (rotation.value + 90) % 360;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="image-viewer">
|
||||||
|
<div class="image-controls">
|
||||||
|
<v-btn icon="mdi-plus" @click="zoomIn" title="Zoom In"></v-btn>
|
||||||
|
<v-btn icon="mdi-minus" @click="zoomOut" title="Zoom Out"></v-btn>
|
||||||
|
<v-btn icon="mdi-rotate-right" @click="rotateImage" title="Rotate"></v-btn>
|
||||||
|
<v-btn icon="mdi-refresh" @click="resetZoom" title="Reset"></v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="image-container">
|
||||||
|
<img :src="src" :style="{
|
||||||
|
transform: `scale(${scale}) rotate(${rotation}deg)`,
|
||||||
|
transition: 'transform 0.2s ease-in-out'
|
||||||
|
}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-viewer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-controls {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,7 +2,6 @@
|
|||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { FilesManager } from '~/composables/files-manager';
|
import { FilesManager } from '~/composables/files-manager';
|
||||||
// @ts-ignore
|
|
||||||
import { iFile } from '~/composables/worker/7zip-manager';
|
import { iFile } from '~/composables/worker/7zip-manager';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -18,6 +17,7 @@ let { file, filesManager } = defineProps<Props>()
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
|
||||||
|
if(!file) return;
|
||||||
let fileContent = await filesManager.getFileContent(file.path);
|
let fileContent = await filesManager.getFileContent(file.path);
|
||||||
monaco.editor.create(editor.value, {
|
monaco.editor.create(editor.value, {
|
||||||
value: fileContent?.toString()!,
|
value: fileContent?.toString()!,
|
||||||
@ -35,6 +35,6 @@ onMounted(async () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
#editor {
|
#editor {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: calc(100vh - 120px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -10,7 +10,14 @@ import mime from 'mime';
|
|||||||
export const videoExtensions = ['mp4', 'avi', 'mov', 'mkv'];
|
export const videoExtensions = ['mp4', 'avi', 'mov', 'mkv'];
|
||||||
export const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
export const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
export const audioExtensions = ['mp3', 'wav', 'ogg', 'flac'];
|
export const audioExtensions = ['mp3', 'wav', 'ogg', 'flac'];
|
||||||
export const textExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'js', 'ts', 'php', 'c', 'cpp', 'py', 'html', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'sql', 'java', 'go', 'rb', 'sh', 'bat', 'ps1', 'cmd', 'yml', 'yaml', 'ini', 'toml', 'csv', 'tsv', 'gitignore', 'lock', 'htaccess', 'htpasswd', 'env', 'dockerfile', 'gitattributes', 'gitmodules', 'editorconfig', 'babelrc', 'eslintrc', 'eslintignore', 'prettierrc', 'prettierignore', 'stylelintrc', 'stylelintignore', 'postcssrc', 'postcss.config', 'jsx', 'tsx', 'license']
|
export const textExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'js', 'ts', 'php', 'c', 'cpp', 'py', 'html', 'css', 'scss', 'sass', 'less', 'json', 'xml', 'sql', 'java', 'go', 'rb', 'sh', 'bat', 'ps1', 'cmd', 'yml', 'yaml', 'ini', 'toml', 'csv', 'tsv', 'gitignore', 'lock', 'htaccess', 'htpasswd', 'env', 'dockerfile', 'gitattributes', 'gitmodules', 'editorconfig', 'babelrc', 'eslintrc', 'eslintignore', 'prettierrc', 'prettierignore', 'stylelintrc', 'stylelintignore', 'postcssrc', 'postcss.config', 'jsx', 'tsx', 'license'];
|
||||||
|
export const binaryExtensions = ['exe', 'dll', 'so', 'dylib', 'bin', 'dat', 'db', 'sqlite', 'o', 'class', 'pyc'];
|
||||||
|
export const supportedExtensions = [
|
||||||
|
'7z', 'xz', 'bz2', 'gz', 'tar', 'zip', 'wim',
|
||||||
|
'apfs', 'ar', 'arj', 'cab', 'chm', 'cpio', 'dmg', 'ext', 'fat', 'gpt', 'hfs',
|
||||||
|
'ihex', 'iso', 'lzh', 'lzma', 'mbr', 'msi', 'nsis', 'ntfs', 'qcow2', 'rar',
|
||||||
|
'rpm', 'squashfs', 'udf', 'uefi', 'vdi', 'vhd', 'vhdx', 'vmdk', 'xar', 'z'
|
||||||
|
];
|
||||||
|
|
||||||
export class FilesManager {
|
export class FilesManager {
|
||||||
consoleOutputBuffer: string[] = [];
|
consoleOutputBuffer: string[] = [];
|
||||||
@ -27,6 +34,12 @@ export class FilesManager {
|
|||||||
|
|
||||||
async loadArchive(file: File) {
|
async loadArchive(file: File) {
|
||||||
if (!this.remoteSevenZipManager) return;
|
if (!this.remoteSevenZipManager) return;
|
||||||
|
|
||||||
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (!extension || !supportedExtensions.includes(extension)) {
|
||||||
|
throw new Error('Unsupported file format. Please use a supported archive format.');
|
||||||
|
}
|
||||||
|
|
||||||
this.filesList.value = await this.remoteSevenZipManager.loadArchive(file) || [];
|
this.filesList.value = await this.remoteSevenZipManager.loadArchive(file) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,9 +166,10 @@ export class SevenZipManager {
|
|||||||
async getFileContent(file: iFile, encoding: "utf8" | "binary" = "utf8") {
|
async getFileContent(file: iFile, encoding: "utf8" | "binary" = "utf8") {
|
||||||
if (!this.sevenZip) return;
|
if (!this.sevenZip) return;
|
||||||
file = typeof file === "string" ? JSON.parse(file) : file;
|
file = typeof file === "string" ? JSON.parse(file) : file;
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
// extract file from archive
|
// extract file from archive
|
||||||
this.execute(['x', '-y', this.archiveName, file.path.substring(1)]);
|
this.execute(['x', '-y', this.archiveName, file?.path.substring(1)]);
|
||||||
this.sevenZip.FS.chmod(file.path, 0o777);
|
this.sevenZip.FS.chmod(file.path, 0o777);
|
||||||
|
|
||||||
// get file buffer
|
// get file buffer
|
||||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -2662,9 +2662,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001487",
|
"version": "1.0.30001677",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz",
|
||||||
"integrity": "sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==",
|
"integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user