mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
first attempt with adding and downloading message attachments
This commit is contained in:
parent
463a1766f1
commit
8d62550775
9 changed files with 206 additions and 121 deletions
|
@ -2,12 +2,12 @@
|
|||
<div class="row justify-content-start mb-2">
|
||||
<div :class="messageWidthClass">
|
||||
<div class="card bg-light border-0 text-dark">
|
||||
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
|
||||
<p class="card-text">
|
||||
{{ getFileContent["filename"] }} |
|
||||
{{ getFileContent["filesize"] }} Bytes |
|
||||
{{ getFileContent["filetype"] }}
|
||||
</p>
|
||||
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id" class="card-header">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button class="btn btn-light text-truncate" disabled>{{ attachment.name }}</button>
|
||||
<button @click="downloadAttachment(attachment.hash_sha512, attachment.name)" class="btn btn-light w-25"><i class="bi bi-download strong"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
@ -33,14 +33,7 @@
|
|||
<i class="bi bi-info-circle"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
disabled
|
||||
v-if="getFileContent['filesize'] !== 0"
|
||||
class="btn btn-outline-secondary border-0 me-1"
|
||||
@click="downloadAttachment"
|
||||
>
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
|
||||
|
||||
<button class="btn btn-outline-secondary border-0" @click="deleteMessage">
|
||||
<i class="bi bi-trash"></i>
|
||||
|
@ -61,7 +54,6 @@ import { atob_FD } from "../js/freedata";
|
|||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { useChatStore } from "../store/chatStore.js";
|
||||
const chat = useChatStore(pinia);
|
||||
|
@ -80,40 +72,42 @@ export default {
|
|||
deleteMessage() {
|
||||
deleteMessageFromDB(this.message.id);
|
||||
},
|
||||
async downloadAttachment() {
|
||||
async downloadAttachment(hash_sha512, fileName) {
|
||||
try {
|
||||
// reset file store
|
||||
chat.downloadFileFromDB = [];
|
||||
const jsondata = await getMessageAttachment(hash_sha512);
|
||||
const byteCharacters = atob(jsondata.data);
|
||||
const byteArrays = [];
|
||||
|
||||
const attachment = await getMessageAttachment(this.message.id);
|
||||
const blob = new Blob([atob_FD(attachment[2])], {
|
||||
type: `${attachment[1]};charset=utf-8`,
|
||||
});
|
||||
window.focus();
|
||||
saveAs(blob, attachment[0]);
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
const blob = new Blob(byteArrays, { type: jsondata.type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Creating a temporary anchor element to download the file
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("Failed to download attachment:", error);
|
||||
console.error("Failed to download the attachment:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
getFileContent() {
|
||||
if (this.message.attachments.length <= 0) {
|
||||
return { filename: "", filesize: 0, filetype: "" };
|
||||
}
|
||||
|
||||
try {
|
||||
var filename = Object.keys(this.message._attachments)[0];
|
||||
var filesize = this.message._attachments[filename]["length"];
|
||||
var filetype = filename.split(".")[1];
|
||||
|
||||
return { filename: filename, filesize: filesize, filetype: filetype };
|
||||
} catch (e) {
|
||||
console.log("file not loaded from database - empty?");
|
||||
// we are only checking against filesize for displaying attachments
|
||||
return { filesize: 0 };
|
||||
}
|
||||
},
|
||||
messageWidthClass() {
|
||||
// Calculate a Bootstrap grid class based on message length
|
||||
// Adjust the logic as needed to fit your requirements
|
||||
|
|
|
@ -2,14 +2,6 @@
|
|||
<div class="row justify-content-end mb-2">
|
||||
<!-- control area -->
|
||||
<div class="col-auto p-0 m-0">
|
||||
<button
|
||||
disabled
|
||||
v-if="getFileContent['filesize'] !== 0"
|
||||
class="btn btn-outline-secondary border-0 me-1"
|
||||
@click="downloadAttachment"
|
||||
>
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-outline-secondary border-0 me-1"
|
||||
|
@ -32,17 +24,17 @@
|
|||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- message area -->
|
||||
<div :class="messageWidthClass">
|
||||
<div class="card bg-secondary text-white">
|
||||
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
|
||||
<p class="card-text">
|
||||
{{ getFileContent["filename"] }} |
|
||||
{{ getFileContent["filesize"] }} Bytes |
|
||||
{{ getFileContent["filetype"] }}
|
||||
</p>
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id" class="card-header">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button class="btn btn-light text-truncate" disabled>{{ attachment.name }}</button>
|
||||
<button @click="downloadAttachment(attachment.hash_sha512, attachment.name)" class="btn btn-light w-25"><i class="bi bi-download strong"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ message.body }}</p>
|
||||
|
@ -101,7 +93,6 @@ import {
|
|||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { useChatStore } from "../store/chatStore.js";
|
||||
const chat = useChatStore(pinia);
|
||||
|
@ -126,42 +117,41 @@ export default {
|
|||
//console.log(this.infoModal)
|
||||
//this.infoModal.show()
|
||||
},
|
||||
async downloadAttachment() {
|
||||
async downloadAttachment(hash_sha512, fileName) {
|
||||
try {
|
||||
// reset file store
|
||||
chat.downloadFileFromDB = [];
|
||||
const jsondata = await getMessageAttachment(hash_sha512);
|
||||
const byteCharacters = atob(jsondata.data);
|
||||
const byteArrays = [];
|
||||
|
||||
const attachment = await getMessageAttachment(this.message.id);
|
||||
const blob = new Blob([atob_FD(attachment[2])], {
|
||||
type: `${attachment[1]};charset=utf-8`,
|
||||
});
|
||||
saveAs(blob, attachment[0]);
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
const blob = new Blob(byteArrays, { type: jsondata.type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Creating a temporary anchor element to download the file
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("Failed to download attachment:", error);
|
||||
console.error("Failed to download the attachment:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
getFileContent() {
|
||||
if (this.message.attachments.length <= 0) {
|
||||
return { filename: "", filesize: 0, filetype: "" };
|
||||
}
|
||||
|
||||
var filename = Object.keys(this.message._attachments)[0];
|
||||
var filesize = this.message._attachments[filename]["length"];
|
||||
var filetype = filename.split(".")[1];
|
||||
|
||||
// ensure filesize is 0 for hiding message header if no data is available
|
||||
if (
|
||||
typeof filename === "undefined" ||
|
||||
filename === "" ||
|
||||
filename === "-"
|
||||
) {
|
||||
filesize = 0;
|
||||
}
|
||||
|
||||
return { filename: filename, filesize: filesize, filetype: filetype };
|
||||
},
|
||||
messageWidthClass() {
|
||||
// Calculate a Bootstrap grid class based on message length
|
||||
// Adjust the logic as needed to fit your requirements
|
||||
|
|
|
@ -44,34 +44,72 @@ chat.inputText += detail.unicode
|
|||
const chatModuleMessage=ref(null);
|
||||
|
||||
|
||||
// Function to trigger the hidden file input
|
||||
function triggerFileInput() {
|
||||
fileInput.value.click();
|
||||
}
|
||||
|
||||
|
||||
function transmitNewMessage(){
|
||||
// Use a ref for storing multiple files
|
||||
const selectedFiles = ref([]);
|
||||
const fileInput = ref(null);
|
||||
|
||||
function handleFileSelection(event) {
|
||||
// Reset previously selected files
|
||||
selectedFiles.value = [];
|
||||
|
||||
// if no callsign is selected, assume we are using the first one..
|
||||
if(typeof(chat.selectedCallsign) == 'undefined'){
|
||||
// Process each file
|
||||
for (let file of event.target.files) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
// Convert file content to base64
|
||||
const base64Content = btoa(reader.result); // Adjust this line if necessary
|
||||
selectedFiles.value.push({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
content: base64Content, // Store base64 encoded content
|
||||
});
|
||||
};
|
||||
reader.readAsBinaryString(file); // Read the file content as binary string
|
||||
}
|
||||
}function transmitNewMessage() {
|
||||
// Check if a callsign is selected, default to the first one if not
|
||||
if (typeof(chat.selectedCallsign) === 'undefined') {
|
||||
chat.selectedCallsign = Object.keys(chat.callsign_list)[0];
|
||||
}
|
||||
|
||||
|
||||
chat.inputText = chat.inputText.trim();
|
||||
if (chat.inputText.length==0 && chat.inputFileName == "-")
|
||||
return;
|
||||
|
||||
// Proceed only if there is text or files selected
|
||||
if (chat.inputText.length === 0 && selectedFiles.value.length === 0) return;
|
||||
|
||||
const attachments = selectedFiles.value.map(file => {
|
||||
return {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file.content
|
||||
};
|
||||
});
|
||||
|
||||
if (chat.selectedCallsign.startsWith("BC-")) {
|
||||
|
||||
return "new broadcast"
|
||||
|
||||
// Handle broadcast message differently if needed
|
||||
return "new broadcast";
|
||||
} else {
|
||||
//newMessage(chat.selectedCallsign, chat.inputText, chat.inputFile, chat.inputFileName, chat.inputFileSize, chat.inputFileType)
|
||||
newMessage(chat.selectedCallsign, chat.inputText)
|
||||
// If there are attachments, send them along with the message
|
||||
if (attachments.length > 0) {
|
||||
newMessage(chat.selectedCallsign, chat.inputText, attachments);
|
||||
} else {
|
||||
// Send text only if no attachments are selected
|
||||
newMessage(chat.selectedCallsign, chat.inputText);
|
||||
}
|
||||
// finally do a cleanup
|
||||
//chatModuleMessage.reset();
|
||||
}
|
||||
|
||||
// Cleanup after sending message
|
||||
chat.inputText = '';
|
||||
chatModuleMessage.value = "";
|
||||
// @ts-expect-error
|
||||
resetFile()
|
||||
selectedFiles.value = []; // Clear selected files after sending
|
||||
// Reset any other states or UI elements as necessary
|
||||
}
|
||||
|
||||
function resetFile(event){
|
||||
|
@ -140,9 +178,9 @@ function calculateTimeNeeded(){
|
|||
return obj.snr === snrList[i].snr
|
||||
})
|
||||
|
||||
calculatedSpeedPerMinutePER0.push(chat.inputFileSize / result.bpm)
|
||||
calculatedSpeedPerMinutePER25.push(chat.inputFileSize / (result.bpm * 0.75))
|
||||
calculatedSpeedPerMinutePER75.push(chat.inputFileSize / (result.bpm * 0.25))
|
||||
calculatedSpeedPerMinutePER0.push(totalSize / result.bpm)
|
||||
calculatedSpeedPerMinutePER25.push(totalSize / (result.bpm * 0.75))
|
||||
calculatedSpeedPerMinutePER75.push(totalSize / (result.bpm * 0.25))
|
||||
|
||||
}
|
||||
|
||||
|
@ -202,11 +240,11 @@ const speedChartData = computed(() => ({
|
|||
|
||||
|
||||
<!-- trigger file selection modal -->
|
||||
<!--
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary border-0 rounded-pill me-1" data-bs-toggle="modal" data-bs-target="#fileSelectionModal">
|
||||
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
|
||||
</button>
|
||||
-->
|
||||
|
||||
|
||||
|
||||
<textarea
|
||||
|
@ -264,20 +302,54 @@ const speedChartData = computed(() => ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!--
|
||||
<div class="input-group-text mb-3">
|
||||
<input class="" type="file" ref="doc" @change="readFile" />
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="container w-100 mb-3">
|
||||
<!-- Button that user will click to open file dialog -->
|
||||
<button class="btn btn-primary w-100" @click="triggerFileInput">Attach Files</button>
|
||||
|
||||
|
||||
<div class="btn-group me-2" role="group" aria-label="Basic outlined example">
|
||||
<button type="button" class="btn btn-secondary">Type</button>
|
||||
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileType}}</button>
|
||||
<!-- Hidden file input -->
|
||||
<input type="file" multiple ref="fileInput" @change="handleFileSelection" style="display: none;" />
|
||||
</div>
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<div class="d-flex flex-row overflow-auto bg-light rounded-3 p-2 border border-1">
|
||||
<div v-for="(file, index) in selectedFiles" :key="index" class="pe-2">
|
||||
<div class="card" style=" min-width: 10rem; max-width: 10rem;">
|
||||
<!-- Card Header with Remove Button -->
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="text-truncate">{{ file.name }}</span>
|
||||
<button class="btn btn-close" @click="removeFile(index)"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">...</p>
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
{{ file.type }}
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
{{ file.size }} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
<div class="btn-group me-2" role="group" aria-label="Basic outlined example">
|
||||
<button type="button" class="btn btn-secondary">Size</button>
|
||||
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileSize}}</button>
|
||||
<button type="button" class="btn btn-secondary">total size</button>
|
||||
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileSize}} {{totalSizeFormatted}}</button>
|
||||
</div>
|
||||
|
||||
<Line :data="speedChartData" />
|
||||
|
|
|
@ -155,10 +155,16 @@ export async function getFreedataMessages() {
|
|||
processFreedataMessages(res);
|
||||
}
|
||||
|
||||
export async function sendFreedataMessage(destination, body) {
|
||||
export async function getFreedataAttachmentBySha512(data_sha512) {
|
||||
let res = await apiGet(`/freedata/messages/attachment/${data_sha512}`);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function sendFreedataMessage(destination, body, attachments) {
|
||||
return await apiPost("/freedata/messages", {
|
||||
destination: destination,
|
||||
body: body,
|
||||
attachments: attachments,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
sendFreedataMessage,
|
||||
deleteFreedataMessage,
|
||||
retransmitFreedataMessage,
|
||||
getFreedataAttachmentBySha512,
|
||||
} from "./api";
|
||||
|
||||
interface Message {
|
||||
|
@ -76,8 +77,9 @@ function createSortedMessagesList(data: {
|
|||
return callsignMessages;
|
||||
}
|
||||
|
||||
export function newMessage(dxcall, body) {
|
||||
sendFreedataMessage(dxcall, body);
|
||||
export function newMessage(dxcall, body, attachments) {
|
||||
console.log(attachments)
|
||||
sendFreedataMessage(dxcall, body, attachments);
|
||||
}
|
||||
|
||||
/* ------ TEMPORARY DUMMY FUNCTIONS --- */
|
||||
|
@ -99,6 +101,6 @@ export function requestMessageInfo(id) {
|
|||
return;
|
||||
}
|
||||
|
||||
export function getMessageAttachment(id) {
|
||||
return;
|
||||
export async function getMessageAttachment(data_sha512) {
|
||||
return await getFreedataAttachmentBySha512(data_sha512)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ def validate_freedata_callsign(callsign):
|
|||
return re.compile(regexp).match(callsign) is not None
|
||||
|
||||
def validate_message_attachment(attachment):
|
||||
print(attachment)
|
||||
for field in ['name', 'type', 'data']:
|
||||
if field not in attachment:
|
||||
raise ValueError(f"Attachment missing '{field}'")
|
||||
|
|
|
@ -64,3 +64,18 @@ class DatabaseManagerAttachments(DatabaseManager):
|
|||
def get_attachments_by_message_id_json(self, message_id):
|
||||
attachments = self.get_attachments_by_message_id(message_id)
|
||||
return json.dumps(attachments)
|
||||
|
||||
def get_attachment_by_sha512(self, hash_sha512):
|
||||
session = self.get_thread_scoped_session()
|
||||
try:
|
||||
attachment = session.query(Attachment).filter_by(hash_sha512=hash_sha512).first()
|
||||
if attachment:
|
||||
return attachment.to_dict() # Assuming you have a to_dict method
|
||||
else:
|
||||
self.log(f"No attachment found with SHA-512 hash: {hash_sha512}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.log(f"Error fetching attachment with SHA-512 hash {hash_sha512}: {e}", isWarning=True)
|
||||
return None
|
||||
finally:
|
||||
session.remove()
|
|
@ -114,7 +114,7 @@ class Attachment(Base):
|
|||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'data_type': self.data_type,
|
||||
'type': self.data_type,
|
||||
'data': self.data,
|
||||
'checksum_crc32': self.checksum_crc32,
|
||||
'hash_sha512' : self.hash_sha512
|
||||
|
|
|
@ -274,6 +274,11 @@ def get_message_attachments(message_id):
|
|||
attachments = DatabaseManagerAttachments(app.event_manager).get_attachments_by_message_id_json(message_id)
|
||||
return api_response(attachments)
|
||||
|
||||
@app.route('/freedata/messages/attachment/<string:data_sha512>', methods=['GET'])
|
||||
def get_message_attachment(data_sha512):
|
||||
attachment = DatabaseManagerAttachments(app.event_manager).get_attachment_by_sha512(data_sha512)
|
||||
return api_response(attachment)
|
||||
|
||||
@app.route('/freedata/beacons', methods=['GET'])
|
||||
def get_all_beacons():
|
||||
beacons = DatabaseManagerBeacon(app.event_manager).get_all_beacons()
|
||||
|
|
Loading…
Reference in a new issue