Added a significant overhaul to the server manager.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Comics Management</h2>
|
||||
<button @click="organizeComics" :disabled="isOrganizing">
|
||||
{{ isOrganizing ? 'Organizing...' : 'Organize Comics Now' }}
|
||||
</button>
|
||||
<button @click="cleanupToberead" :disabled="isCleaning">
|
||||
{{ isCleaning ? 'Cleaning...' : 'Cleanup Folders' }}
|
||||
</button>
|
||||
<p v-if="message">{{ message }}</p>
|
||||
<p v-if="cleanupMessage">{{ cleanupMessage }}</p>
|
||||
<p v-if="error" style="color: red;">Error: {{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isOrganizing = ref(false);
|
||||
const message = ref('');
|
||||
const error = ref('');
|
||||
const isCleaning = ref(false);
|
||||
const cleanupMessage = ref('');
|
||||
|
||||
const organizeComics = async () => {
|
||||
isOrganizing.value = true;
|
||||
message.value = 'Initiating comics organization... This may take a while.';
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/comics/organize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
message.value = data.message || 'Comics organization process started successfully!';
|
||||
console.log('Backend response:', data);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
error.value = errorData.detail || `Failed to organize comics: ${response.status}`;
|
||||
console.error('Error response from backend:', errorData);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = `Network error: ${err.message}`;
|
||||
console.error('Network or unexpected error:', err);
|
||||
} finally {
|
||||
isOrganizing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupToberead = async () => {
|
||||
if (window.confirm("This will delete empty folders in the 'toberead' path. Are you sure you want to continue?")) {
|
||||
isCleaning.value = true;
|
||||
cleanupMessage.value = 'Initiating cleanup...';
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/comics/cleanup_toberead', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
cleanupMessage.value = data.message || 'Cleanup process started successfully!';
|
||||
console.log('Backend response:', data);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
error.value = errorData.detail || `Failed to start cleanup: ${response.status}`;
|
||||
console.error('Error response from backend:', errorData);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = `Network error: ${err.message}`;
|
||||
console.error('Network or unexpected error:', err);
|
||||
} finally {
|
||||
isCleaning.value = false;
|
||||
}
|
||||
} else {
|
||||
cleanupMessage.value = 'Cleanup cancelled.';
|
||||
isCleaning.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div v-if="filePath" class="modal-overlay" @click.self="close">
|
||||
<div class="modal-content">
|
||||
<button class="close-button" @click="close">×</button>
|
||||
<h3>{{ fileName }}</h3>
|
||||
<div class="viewer-container">
|
||||
<img v-if="fileType === 'image'" :src="fileUrl" :alt="fileName" class="viewable-image" />
|
||||
<video v-if="fileType === 'video'" :src="fileUrl" controls class="viewable-video"></video>
|
||||
<audio v-if="fileType === 'audio'" :src="fileUrl" controls class="viewable-audio"></audio>
|
||||
<pre v-if="fileType === 'text'"><code>{{ textContent }}</code></pre>
|
||||
<div v-if="fileType === 'cbz'">
|
||||
<div v-if="cbzImageUrls.length > 0">
|
||||
<img :src="cbzImageUrls[currentCbzImageIndex]" class="viewable-image" />
|
||||
<div>
|
||||
<button @click="prevCbzImage" :disabled="currentCbzImageIndex === 0">Previous</button>
|
||||
<span>{{ currentCbzImageIndex + 1 }} / {{ cbzImageUrls.length }}</span>
|
||||
<button @click="nextCbzImage" :disabled="currentCbzImageIndex === cbzImageUrls.length - 1">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>Loading CBZ content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
filePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
shareName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const fileName = computed(() => props.filePath.split('/').pop());
|
||||
const fileUrl = computed(() => `/api/samba/view/${props.shareName}?path=${props.filePath}`);
|
||||
const textContent = ref('');
|
||||
const cbzImageUrls = ref([]);
|
||||
const currentCbzImageIndex = ref(0);
|
||||
|
||||
const fileType = computed(() => {
|
||||
const extension = props.filePath.split('.').pop().toLowerCase();
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(extension)) {
|
||||
return 'image';
|
||||
}
|
||||
if (['mp4', 'mkv', 'avi', 'mov', 'webm'].includes(extension)) {
|
||||
return 'video';
|
||||
}
|
||||
if (['mp3', 'flac', 'wav', 'ogg'].includes(extension)) {
|
||||
return 'audio';
|
||||
}
|
||||
if (['txt', 'py', 'js', 'json', 'xml', 'html', 'css', 'md'].includes(extension)) {
|
||||
return 'text';
|
||||
}
|
||||
if (extension === 'cbz') {
|
||||
return 'cbz';
|
||||
}
|
||||
return 'other';
|
||||
});
|
||||
|
||||
watchEffect(async () => {
|
||||
if (fileType.value === 'text') {
|
||||
try {
|
||||
const response = await fetch(fileUrl.value);
|
||||
if (response.ok) {
|
||||
textContent.value = await response.text();
|
||||
} else {
|
||||
textContent.value = 'Error loading file content.';
|
||||
}
|
||||
} catch (error) {
|
||||
textContent.value = 'Error loading file content.';
|
||||
console.error('Error fetching text content:', error);
|
||||
}
|
||||
} else if (fileType.value === 'cbz') {
|
||||
try {
|
||||
const response = await fetch(`/api/samba/cbz/list/${props.shareName}?path=${props.filePath}`);
|
||||
if (response.ok) {
|
||||
const imageNames = await response.json();
|
||||
cbzImageUrls.value = imageNames.map(name => `/api/samba/cbz/image/${props.shareName}?path=${props.filePath}&image_name=${encodeURIComponent(name)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching CBZ content:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const prevCbzImage = () => {
|
||||
if (currentCbzImageIndex.value > 0) {
|
||||
currentCbzImageIndex.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const nextCbzImage = () => {
|
||||
if (currentCbzImageIndex.value < cbzImageUrls.value.length - 1) {
|
||||
currentCbzImageIndex.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.viewer-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.viewable-image,
|
||||
.viewable-video,
|
||||
.viewable-audio {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #2d2d2d; /* Dark background */
|
||||
color: #f8f8f2; /* Light text color */
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Samba File Explorer</h2>
|
||||
|
||||
<label for="share-select">Select Share:</label>
|
||||
<select id="share-select" v-model="shareName" @change="resetAndFetch">
|
||||
<option v-for="share in availableShares" :key="share" :value="share">{{ share }}</option>
|
||||
</select>
|
||||
|
||||
<p>Current Path: {{ shareName }}:{{ currentPath }}</p>
|
||||
<ul>
|
||||
<li v-for="item in files" :key="item.name">
|
||||
<span @click="item.is_directory ? navigate(item) : viewFile(item.path)">
|
||||
{{ item.name }} {{ item.is_directory ? '/' : '' }}
|
||||
</span>
|
||||
<button v-if="!item.is_directory" @click="viewFile(item.path)">View</button>
|
||||
<button v-if="!item.is_directory" @click="downloadFile(item.path)">Download</button>
|
||||
<button @click="renameItem(item.path, item.name, item.is_directory)">Rename</button>
|
||||
<button @click="deleteItem(item.path, item.is_directory)">Delete</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button @click="goBack" :disabled="currentPath === '/'">Go Back</button>
|
||||
<form @submit.prevent="uploadFile">
|
||||
<input type="file" @change="handleFileUpload" />
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
|
||||
<FileViewer
|
||||
v-if="fileToView"
|
||||
:file-path="fileToView"
|
||||
:share-name="shareName"
|
||||
@close="closeViewer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import FileViewer from './FileViewer.vue';
|
||||
|
||||
const availableShares = ref(['media', 'isolation']);
|
||||
const shareName = ref(availableShares.value[0]);
|
||||
const currentPath = ref('/');
|
||||
const files = ref([]);
|
||||
const fileToUpload = ref(null);
|
||||
const fileToView = ref(null);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/samba/list/${shareName.value}?path=${currentPath.value}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
files.value = data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching files:', error);
|
||||
files.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const resetAndFetch = () => {
|
||||
currentPath.value = '/';
|
||||
fetchFiles();
|
||||
};
|
||||
|
||||
const navigate = (item) => {
|
||||
if (item.is_directory) {
|
||||
currentPath.value = item.path;
|
||||
fetchFiles();
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
const parts = currentPath.value.split('/').filter(Boolean);
|
||||
if (parts.length > 0) {
|
||||
currentPath.value = '/' + parts.slice(0, -1).join('/') + (parts.length > 1 ? '/' : '');
|
||||
} else {
|
||||
currentPath.value = '/';
|
||||
}
|
||||
fetchFiles();
|
||||
};
|
||||
|
||||
const viewFile = (filePath) => {
|
||||
fileToView.value = filePath;
|
||||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
fileToView.value = null;
|
||||
};
|
||||
|
||||
const downloadFile = async (filePath) => {
|
||||
try {
|
||||
const response = await fetch(`/api/samba/files/download/${shareName.value}?path=${filePath}`);
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filePath.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = async (itemPath, isDirectory) => {
|
||||
if (!confirm(`Are you sure you want to delete ${itemPath}?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/api/samba/files/${shareName.value}?path=${itemPath}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (response.ok) {
|
||||
console.log(`${itemPath} deleted successfully.`);
|
||||
fetchFiles();
|
||||
} else {
|
||||
console.error('Error deleting item:', await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renameItem = async (oldPath, oldName, isDirectory) => {
|
||||
const newName = prompt(`Enter new name for ${oldName}:`, oldName);
|
||||
if (!newName || newName === oldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPath = currentPath.value === '/' ? `/${newName}` : `${currentPath.value.replace(/\/$/, '')}/${newName}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/samba/files/rename/${shareName.value}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ old_path: oldPath, new_path: newPath }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`${oldName} renamed to ${newName} successfully.`);
|
||||
fetchFiles();
|
||||
} else {
|
||||
console.error('Error renaming item:', await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renaming item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (event) => {
|
||||
fileToUpload.value = event.target.files[0];
|
||||
};
|
||||
|
||||
const uploadFile = async () => {
|
||||
if (!fileToUpload.value) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToUpload.value);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/samba/files/upload/${shareName.value}?path=${currentPath.value}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
if (response.ok) {
|
||||
console.log('File uploaded successfully.');
|
||||
fileToUpload.value = null;
|
||||
fetchFiles();
|
||||
} else {
|
||||
console.error('Error uploading file:', await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchFiles();
|
||||
});
|
||||
|
||||
watch(shareName, () => {
|
||||
resetAndFetch();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add some basic styling */
|
||||
div {
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
li span {
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
color: #007bff;
|
||||
}
|
||||
li span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
button {
|
||||
margin-left: 5px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Video Duplicates Scanner</h2>
|
||||
<button @click="startScan" :disabled="isLoading">Start Scan</button>
|
||||
<button @click="pauseScan" :disabled="isLoading">Pause Scan</button>
|
||||
<button @click="resumeScan" :disabled="isLoading">Resume Scan</button>
|
||||
<p>Status: {{ status }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const status = ref('Idle');
|
||||
const isLoading = ref(false);
|
||||
|
||||
const getStatus = async () => {
|
||||
// This endpoint doesn't exist yet, but I'm adding the function in preparation.
|
||||
// try {
|
||||
// const response = await fetch('/api/duplicates/scan/status');
|
||||
// if (response.ok) {
|
||||
// const data = await response.json();
|
||||
// status.value = data.status;
|
||||
// } else {
|
||||
// status.value = 'Error fetching status';
|
||||
// }
|
||||
// } catch (error) {
|
||||
// status.value = 'Error fetching status';
|
||||
// console.error('Error fetching status:', error);
|
||||
// }
|
||||
};
|
||||
|
||||
const startScan = async () => {
|
||||
isLoading.value = true;
|
||||
status.value = 'Starting scan...';
|
||||
try {
|
||||
const response = await fetch('/api/duplicates/scan', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
status.value = 'Scan started.';
|
||||
} else {
|
||||
status.value = 'Error starting scan.';
|
||||
}
|
||||
} catch (error) {
|
||||
status.value = 'Error starting scan.';
|
||||
console.error('Error starting scan:', error);
|
||||
}
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
const pauseScan = async () => {
|
||||
isLoading.value = true;
|
||||
status.value = 'Pausing scan...';
|
||||
try {
|
||||
const response = await fetch('/api/duplicates/scan/pause', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
status.value = 'Scan paused.';
|
||||
} else {
|
||||
status.value = 'Error pausing scan.';
|
||||
}
|
||||
} catch (error) {
|
||||
status.value = 'Error pausing scan.';
|
||||
console.error('Error pausing scan:', error);
|
||||
}
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
const resumeScan = async () => {
|
||||
isLoading.value = true;
|
||||
status.value = 'Resuming scan...';
|
||||
try {
|
||||
const response = await fetch('/api/duplicates/scan/resume', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
status.value = 'Scan resumed.';
|
||||
} else {
|
||||
status.value = 'Error resuming scan.';
|
||||
}
|
||||
} catch (error) {
|
||||
status.value = 'Error resuming scan.';
|
||||
console.error('Error resuming scan:', error);
|
||||
}
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// getStatus();
|
||||
// setInterval(getStatus, 5000); // Poll for status every 5 seconds
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user