Document Preview
This commit is contained in:
parent
bfd7890cbc
commit
c93d208990
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "Universal Attachment Preview",
|
||||
"version": "1.0.0",
|
||||
"summary": "Add a View (eye) button to attachments and preview any file (images, pdf, video, audio, office via conversion).",
|
||||
"description": "Universal previewer for attachments. Uses LibreOffice for office conversion to PDF.",
|
||||
"author": "You",
|
||||
"license": "AGPL-3",
|
||||
"category": "Tools",
|
||||
"depends": ["base", "web", "mail", "documents", "onlyoffice_odoo"], # documents optional but helpful
|
||||
"data": [
|
||||
# "security/ir.model.access.csv",
|
||||
# "views/preview_wizard_views.xml",
|
||||
# "views/assets.xml",
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
# "universal_attachment_preview/static/src/css/attachment_preview.css",
|
||||
"universal_attachment_preview/static/src/xml/binary_field_inherit.xml",
|
||||
"universal_attachment_preview/static/src/attachment_preview_popup/attachment_preview_popup.xml",
|
||||
"universal_attachment_preview/static/src/attachment_preview_popup/attachment_preview_popup.js",
|
||||
"universal_attachment_preview/static/src/js/binary_file_preview.js",
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
"installable": True,
|
||||
"application": False,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
# file: my_module/controllers/main.py
|
||||
import base64
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import markupsafe
|
||||
|
||||
import json
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
import logging
|
||||
from odoo.addons.onlyoffice_odoo.controllers.controllers import Onlyoffice_Connector
|
||||
from odoo.addons.onlyoffice_odoo.utils import config_utils, file_utils, jwt_utils, url_utils
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_mobile_regex = r"android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino" # noqa: E501
|
||||
import mimetypes
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Reliable MIME → EXT mapping for Office, PDF, others
|
||||
# ---------------------------------------------------------
|
||||
OFFICE_MAPPING = {
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template": ".dotx",
|
||||
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template": ".xltx",
|
||||
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow": ".ppsx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template": ".potx",
|
||||
|
||||
"application/pdf": ".pdf",
|
||||
}
|
||||
|
||||
def _get_extension_from_mimetype(mimetype):
|
||||
if not mimetype:
|
||||
return None
|
||||
|
||||
# 1️⃣ Exact match for Office formats
|
||||
if mimetype in OFFICE_MAPPING:
|
||||
return OFFICE_MAPPING[mimetype]
|
||||
|
||||
# 2️⃣ Try Python's builtin guess
|
||||
ext = mimetypes.guess_extension(mimetype)
|
||||
if ext:
|
||||
return ext
|
||||
|
||||
# 3️⃣ Fallback
|
||||
return None
|
||||
|
||||
|
||||
|
||||
class AttachmentPreview(http.Controller):
|
||||
|
||||
|
||||
@http.route("/lookup_or_create/attachment", type="json", auth="user")
|
||||
def lookup_or_create_attachment(self, model, res_id, field):
|
||||
record = request.env[model].sudo().browse(int(res_id))
|
||||
if not record.exists():
|
||||
return {"error": "Record not found"}
|
||||
|
||||
# Check existing attachment
|
||||
attach = request.env["ir.attachment"].sudo().search([
|
||||
("res_model", "=", model),
|
||||
("res_id", "=", int(res_id)),
|
||||
("res_field", "=", field)
|
||||
], limit=1)
|
||||
|
||||
# Create attachment if missing
|
||||
if not attach:
|
||||
binary_data = getattr(record, field)
|
||||
if not binary_data:
|
||||
return {"error": "Binary field empty"}
|
||||
|
||||
filename = record._fields[field].string or "file.bin"
|
||||
|
||||
attach = request.env["ir.attachment"].sudo().create({
|
||||
"name": filename,
|
||||
"datas": binary_data,
|
||||
"res_model": model,
|
||||
"res_id": int(res_id),
|
||||
"res_field": field,
|
||||
"type": "binary",
|
||||
})
|
||||
|
||||
return {
|
||||
"attachment_id": attach.id,
|
||||
"name": attach.name,
|
||||
"mimetype": attach.mimetype,
|
||||
"url": f"/web/content/{attach.id}?download=false",
|
||||
}
|
||||
|
||||
def convert_office_to_pdf(self,data, ext):
|
||||
"""Convert office files to PDF using LibreOffice."""
|
||||
with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as f:
|
||||
f.write(data)
|
||||
input_path = f.name
|
||||
|
||||
output_path = input_path.replace(f".{ext}", ".pdf")
|
||||
|
||||
subprocess.run([
|
||||
"libreoffice",
|
||||
"--headless",
|
||||
"--convert-to", "pdf",
|
||||
"--outdir", os.path.dirname(input_path),
|
||||
input_path
|
||||
], check=True)
|
||||
|
||||
with open(output_path, "rb") as f:
|
||||
pdf_data = f.read()
|
||||
|
||||
return pdf_data
|
||||
|
||||
|
||||
@http.route("/universal_preview/get_file",type="json",auth="user",methods=["POST"],)
|
||||
def get_file(self, model, res_id, field):
|
||||
"""
|
||||
Input JSON:
|
||||
{
|
||||
"model": "res.partner",
|
||||
"res_id": 12,
|
||||
"field": "x_document"
|
||||
}
|
||||
"""
|
||||
# model = model
|
||||
# res_id = res_id
|
||||
# field = field
|
||||
|
||||
record = request.env[model].sudo().browse(res_id)
|
||||
if not record.exists():
|
||||
return {"error": "Record not found"}
|
||||
|
||||
attachment = request.env["ir.attachment"].sudo().search([
|
||||
("res_model", "=", model),
|
||||
("res_id", "=", res_id),
|
||||
("res_field", "=", field)
|
||||
], limit=1)
|
||||
|
||||
if not attachment:
|
||||
return {"error": "Attachment not found"}
|
||||
|
||||
mimetype = attachment.mimetype
|
||||
file_data = base64.b64decode(attachment.datas)
|
||||
filename = attachment.name.lower()
|
||||
|
||||
# office → pdf conversion
|
||||
office_map = {
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
||||
"application/msword": "doc",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
||||
"application/vnd.ms-excel": "xls",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
||||
"application/vnd.ms-powerpoint": "ppt",
|
||||
}
|
||||
|
||||
ext = office_map.get(mimetype)
|
||||
|
||||
if ext:
|
||||
try:
|
||||
pdf_data = self.convert_office_to_pdf(file_data, ext)
|
||||
return {
|
||||
"filename": filename.replace(f".{ext}", ".pdf"),
|
||||
"mimetype": "application/pdf",
|
||||
"data": base64.b64encode(pdf_data).decode()
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Conversion failed: {str(e)}"}
|
||||
|
||||
# return original file
|
||||
return {
|
||||
"filename": filename,
|
||||
"mimetype": mimetype,
|
||||
"data": attachment.datas,
|
||||
}
|
||||
|
||||
class OnlyofficeConnector(Onlyoffice_Connector):
|
||||
|
||||
@http.route("/onlyoffice/editor/<int:attachment_id>", auth="public", type="http", website=True)
|
||||
def render_editor(self, attachment_id, access_token=None, readonly=False):
|
||||
_logger.info("GET /onlyoffice/editor/%s", attachment_id)
|
||||
attachment = self.get_attachment(attachment_id)
|
||||
if not attachment:
|
||||
_logger.warning("GET /onlyoffice/editor/%s - attachment not found", attachment_id)
|
||||
return request.not_found()
|
||||
|
||||
attachment.validate_access(access_token)
|
||||
|
||||
if attachment.res_model == "documents.document":
|
||||
document = request.env["documents.document"].browse(int(attachment.res_id))
|
||||
self._check_document_access(document)
|
||||
|
||||
data = attachment.read(["id", "checksum", "public", "name", "access_token"])[0]
|
||||
filename = data["name"]
|
||||
|
||||
can_read = attachment.has_access("read") and file_utils.can_view(filename)
|
||||
if readonly:
|
||||
can_read = attachment.has_access("read")
|
||||
can_write = not readonly and (attachment.has_access("write") and file_utils.can_edit(filename))
|
||||
|
||||
if not can_read:
|
||||
_logger.warning("GET /onlyoffice/editor/%s - no read access", attachment_id)
|
||||
raise Exception("cant read")
|
||||
|
||||
_logger.info("GET /onlyoffice/editor/%s - success", attachment_id)
|
||||
return request.render(
|
||||
"onlyoffice_odoo.onlyoffice_editor", self.prepare_editor_values(attachment, access_token, can_write)
|
||||
)
|
||||
#
|
||||
# def prepare_editor_values(self, attachment, access_token, can_write):
|
||||
# import pdb
|
||||
# pdb.set_trace()
|
||||
# _logger.info("prepare_editor_values - attachment: %s", attachment.id)
|
||||
# data = attachment.read(["id", "checksum", "public", "name", "access_token", "mimetype"])[0]
|
||||
#
|
||||
# filename = data.get("name") or ""
|
||||
# mimetype = attachment.mimetype or ""
|
||||
#
|
||||
# # Extract current extension (if exists)
|
||||
# current_ext = ""
|
||||
# if "." in filename:
|
||||
# current_ext = "." + filename.split(".")[-1].lower()
|
||||
#
|
||||
# # Compute correct extension from mimetype
|
||||
# correct_ext = _get_extension_from_mimetype(mimetype)
|
||||
#
|
||||
# # -----------------------------------------------------
|
||||
# # FIX #1: filename has NO extension → add correct ext
|
||||
# # FIX #2: filename has WRONG extension → replace it
|
||||
# # -----------------------------------------------------
|
||||
# if not correct_ext:
|
||||
# # fallback for unknown mimetypes
|
||||
# correct_ext = ".bin"
|
||||
#
|
||||
# if not filename or "." not in filename:
|
||||
# # No extension → set new name
|
||||
# new_name = f"{attachment.res_field}_{attachment.id}{correct_ext}"
|
||||
# attachment.write({"name": new_name})
|
||||
# data["name"] = new_name
|
||||
#
|
||||
# else:
|
||||
# # Has an extension → check if it matches
|
||||
# if current_ext != correct_ext:
|
||||
# base = filename.rsplit(".", 1)[0]
|
||||
# new_name = f"{base}{correct_ext}"
|
||||
# attachment.write({"name": new_name})
|
||||
# data["name"] = new_name
|
||||
# else:
|
||||
# # extension is correct
|
||||
# data["name"] = filename
|
||||
#
|
||||
# # Continue your original logic without changes
|
||||
# key = str(data["id"]) + str(data["checksum"])
|
||||
# docserver_url = config_utils.get_doc_server_public_url(request.env)
|
||||
# odoo_url = config_utils.get_base_or_odoo_url(request.env)
|
||||
#
|
||||
# filename = self.filter_xss(data["name"])
|
||||
#
|
||||
# security_token = jwt_utils.encode_payload(
|
||||
# request.env, {"id": request.env.user.id}, config_utils.get_internal_jwt_secret(request.env)
|
||||
# )
|
||||
# security_token = security_token.decode("utf-8") if isinstance(security_token, bytes) else security_token
|
||||
# access_token = access_token.decode("utf-8") if isinstance(access_token, bytes) else access_token
|
||||
#
|
||||
# path_part = (
|
||||
# str(data["id"])
|
||||
# + "?oo_security_token="
|
||||
# + security_token
|
||||
# + ("&access_token=" + access_token if access_token else "")
|
||||
# + "&shardkey="
|
||||
# + key
|
||||
# )
|
||||
#
|
||||
# document_type = file_utils.get_file_type(filename)
|
||||
# is_mobile = bool(re.search(_mobile_regex, request.httprequest.headers.get("User-Agent"), re.IGNORECASE))
|
||||
#
|
||||
# root_config = {
|
||||
# "width": "100%",
|
||||
# "height": "100%",
|
||||
# "type": "mobile" if is_mobile else "desktop",
|
||||
# "documentType": document_type,
|
||||
# "document": {
|
||||
# "title": filename,
|
||||
# "url": odoo_url + "onlyoffice/file/content/" + path_part,
|
||||
# "fileType": file_utils.get_file_ext(filename),
|
||||
# "key": key,
|
||||
# "permissions": {},
|
||||
# },
|
||||
# "editorConfig": {
|
||||
# "lang": request.env.user.lang,
|
||||
# "user": {"id": str(request.env.user.id), "name": request.env.user.name},
|
||||
# "customization": {},
|
||||
# },
|
||||
# }
|
||||
#
|
||||
# if can_write:
|
||||
# root_config["editorConfig"]["callbackUrl"] = odoo_url + "onlyoffice/editor/callback/" + path_part
|
||||
#
|
||||
# if attachment.res_model != "documents.document":
|
||||
# root_config["editorConfig"]["mode"] = "edit" if can_write else "view"
|
||||
# root_config["document"]["permissions"]["edit"] = can_write
|
||||
# else:
|
||||
# root_config = self.get_documents_permissions(attachment, can_write, root_config)
|
||||
#
|
||||
# if jwt_utils.is_jwt_enabled(request.env):
|
||||
# root_config["token"] = jwt_utils.encode_payload(request.env, root_config)
|
||||
#
|
||||
# _logger.info("prepare_editor_values - success: %s", attachment.id)
|
||||
# return {
|
||||
# "docTitle": filename,
|
||||
# "docIcon": f"/onlyoffice_odoo/static/description/editor_icons/{document_type}.ico",
|
||||
# "docApiJS": docserver_url + "web-apps/apps/api/documents/api.js",
|
||||
# "editorConfig": markupsafe.Markup(json.dumps(root_config)),
|
||||
# }
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import ir_attachment
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
from odoo import models
|
||||
import mimetypes
|
||||
import base64
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
def _detect_mimetype(self, datas):
|
||||
# direct copy of the above function
|
||||
try:
|
||||
raw = base64.b64decode(datas)
|
||||
except:
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
# office zip check
|
||||
if raw.startswith(b"PK"):
|
||||
try:
|
||||
z = zipfile.ZipFile(io.BytesIO(raw))
|
||||
names = z.namelist()
|
||||
|
||||
if "ppt/presentation.xml" in names:
|
||||
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
if "word/document.xml" in names:
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
if "xl/workbook.xml" in names:
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
return "application/zip"
|
||||
except:
|
||||
pass
|
||||
|
||||
if raw.startswith(b"%PDF-"):
|
||||
return "application/pdf"
|
||||
|
||||
return "application/octet-stream"
|
||||
|
||||
def _get_extension_from_mimetype(self, mimetype):
|
||||
mapping = {
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||
"application/pdf": ".pdf",
|
||||
"application/zip": ".zip",
|
||||
}
|
||||
return mapping.get(mimetype, False)
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# 1) Fix incorrect mimetype (ex: .pptx → zip)
|
||||
# ----------------------------------------------------
|
||||
if "datas" in vals or "mimetype" in vals:
|
||||
for rec in self:
|
||||
datas = vals.get("datas", rec.datas)
|
||||
mimetype = self._detect_mimetype(datas)
|
||||
|
||||
# Update mimetype if wrong
|
||||
if mimetype != rec.mimetype:
|
||||
rec.sudo().write({"mimetype": mimetype})
|
||||
|
||||
# ----------------------------------------------------
|
||||
# 2) Fix the filename extension based on correct mimetype
|
||||
# ----------------------------------------------------
|
||||
# Detect extension from mimetype
|
||||
correct_ext = self._get_extension_from_mimetype(mimetype) or ".bin"
|
||||
|
||||
filename = rec.name or ""
|
||||
current_ext = ""
|
||||
if "." in filename:
|
||||
current_ext = "." + filename.split(".")[-1].lower()
|
||||
|
||||
# CASE 1: No filename or no extension
|
||||
if not filename or "." not in filename:
|
||||
new_name = f"{rec.res_field or 'file'}_{rec.id}{correct_ext}"
|
||||
rec.sudo().write({"name": new_name})
|
||||
|
||||
# CASE 2: Wrong extension → Replace it
|
||||
elif current_ext != correct_ext:
|
||||
base = filename.rsplit(".", 1)[0]
|
||||
new_name = f"{base}{correct_ext}"
|
||||
rec.sudo().write({"name": new_name})
|
||||
|
||||
return res
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# models/preview_wizard.py
|
||||
from odoo import models, fields, api
|
||||
|
||||
class AttachmentPreviewWizard(models.TransientModel):
|
||||
_name = 'attachment.preview.wizard'
|
||||
_description = 'Attachment Preview Wizard'
|
||||
|
||||
attachment_id = fields.Many2one('ir.attachment', string='Attachment', required=True)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_attachment_preview,access_attachment_preview,model_attachment_preview_wizard,base.group_user,1,0,0,0
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import base64
|
||||
from odoo import models, api
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class AttachmentConverter(models.AbstractModel):
|
||||
_name = 'ir.attachment.converter'
|
||||
_description = 'Attachment Converter'
|
||||
|
||||
@api.model
|
||||
def convert_to_pdf(self, attachment):
|
||||
if attachment._get_preview_type() != 'office':
|
||||
return False
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as doc_file:
|
||||
doc_file.write(base64.b64decode(attachment.datas))
|
||||
doc_path = doc_file.name
|
||||
|
||||
pdf_path = doc_path + '.pdf'
|
||||
|
||||
try:
|
||||
# Using LibreOffice for conversion
|
||||
subprocess.run([
|
||||
'libreoffice', '--headless', '--convert-to', 'pdf',
|
||||
'--outdir', os.path.dirname(doc_path), doc_path
|
||||
], check=True)
|
||||
|
||||
with open(pdf_path, 'rb') as pdf_file:
|
||||
return pdf_file.read()
|
||||
except Exception as e:
|
||||
_logger.error(f"Conversion failed: {e}")
|
||||
return False
|
||||
finally:
|
||||
# Clean up temp files
|
||||
if os.path.exists(doc_path):
|
||||
os.unlink(doc_path)
|
||||
if os.path.exists(pdf_path):
|
||||
os.unlink(pdf_path)
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class AttachmentPreviewPopup extends Component {
|
||||
static template = "universal_attachment_preview.PopupPreview";
|
||||
|
||||
// FIX: Register Dialog here
|
||||
static components = { Dialog };
|
||||
|
||||
static props = {
|
||||
attachmentId: Number,
|
||||
url: String,
|
||||
filename: String,
|
||||
mimetype: String,
|
||||
dialogClass: String,
|
||||
close: Function,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.currentViewer = "onlyoffice"; // default viewer
|
||||
}
|
||||
|
||||
// Google Docs Viewer
|
||||
get googleDocsUrl() {
|
||||
return `https://docs.google.com/viewer?url=${encodeURIComponent(this.props.url)}&embedded=true`;
|
||||
}
|
||||
|
||||
// Microsoft Office Viewer
|
||||
get microsoftOfficeUrl() {
|
||||
return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(this.props.url)}`;
|
||||
}
|
||||
|
||||
// OnlyOffice Viewer
|
||||
get onlyofficeUrl() {
|
||||
// return `/onlyoffice/editor/148908?readonly=True`;
|
||||
let onlyOfficeLink = `/onlyoffice/editor/${this.props.attachmentId}?readonly=true`;
|
||||
console.log(onlyOfficeLink);
|
||||
return onlyOfficeLink;
|
||||
}
|
||||
|
||||
get currentViewerUrl() {
|
||||
if (this.currentViewer === "onlyoffice") return this.onlyofficeUrl;
|
||||
if (this.currentViewer === "google") return this.googleDocsUrl;
|
||||
if (this.currentViewer === "microsoft") return this.microsoftOfficeUrl;
|
||||
}
|
||||
|
||||
switchViewer(viewer) {
|
||||
this.currentViewer = viewer;
|
||||
this.render(); // update iframe
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="universal_attachment_preview.PopupPreview">
|
||||
<Dialog title="props.filename" size="'lg'">
|
||||
<t t-if="props.mimetype.endsWith('pdf')">
|
||||
<iframe t-att-src="props.url"
|
||||
style="width:100%; height:500px;"
|
||||
frameborder="0">
|
||||
</iframe>
|
||||
|
||||
</t>
|
||||
<t t-elif="props.mimetype.startsWith('image')">
|
||||
<img t-att-src="props.url"
|
||||
style="max-width:100%; max-height:80vh;" />
|
||||
</t>
|
||||
<t t-elif="
|
||||
props.mimetype.startsWith('audio') or
|
||||
props.mimetype.endsWith('mp3') or
|
||||
props.mimetype.endsWith('wav') or
|
||||
props.mimetype.endsWith('ogg') or
|
||||
props.filename.endsWith('.mp3') or
|
||||
props.filename.endsWith('.wav') or
|
||||
props.filename.endsWith('.ogg')
|
||||
">
|
||||
<audio controls="controls" style="width:100%; margin-top:10px;">
|
||||
<source t-att-src="props.url"/>
|
||||
</audio>
|
||||
</t>
|
||||
|
||||
<t t-elif="props.mimetype.startsWith('video') or props.mimetype.endsWith('mp4') or props.mimetype.endsWith('stream')">
|
||||
<video class="o-FileViewer-view" style="width:100%; height:auto; max-height:80vh;" t-on-click.stop="" controls="controls">
|
||||
<source t-att-src="props.url" />
|
||||
</video>
|
||||
</t>
|
||||
<t t-elif="
|
||||
props.mimetype.startsWith('application/vnd.ms-') or
|
||||
props.mimetype.startsWith('application/vnd.openxmlformats-officedocument.') or
|
||||
props.filename.endsWith('.doc') or
|
||||
props.filename.endsWith('.docx') or
|
||||
props.filename.endsWith('.xls') or
|
||||
props.filename.endsWith('.xlsx') or
|
||||
props.filename.endsWith('.ppt') or
|
||||
props.filename.endsWith('.pptx')
|
||||
">
|
||||
<div class="o_document_preview">
|
||||
<iframe t-att-src="currentViewerUrl"
|
||||
style="width:100%; height:500px;"
|
||||
frameborder="0">
|
||||
</iframe>
|
||||
<div class="text-center mt-2">
|
||||
<button class="btn btn-secondary mr-2"
|
||||
t-att-class="{active: currentViewer === 'onlyoffice'}"
|
||||
t-on-click="() => this.switchViewer('onlyoffice')">
|
||||
OnlyOffice
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary mr-2"
|
||||
t-att-class="{active: currentViewer === 'google'}"
|
||||
t-on-click="() => this.switchViewer('google')">
|
||||
Google Docs
|
||||
</button>
|
||||
<button class="btn btn-secondary mr-2"
|
||||
t-att-class="{active: currentViewer === 'microsoft'}"
|
||||
t-on-click="() => this.switchViewer('microsoft')">
|
||||
Microsoft Office
|
||||
</button>
|
||||
|
||||
<a t-att-href="props.url" target="_blank" class="btn btn-primary">
|
||||
<i class="fa fa-download"/>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <iframe t-att-src="'https://docs.google.com/viewer?url=' + encodeURIComponent(props.url) + '&embedded=true'"-->
|
||||
<!-- style="width:100%; height:500px;"-->
|
||||
<!-- frameborder="0">-->
|
||||
<!-- </iframe>-->
|
||||
<!-- Alternative using Microsoft Office Online -->
|
||||
<!-- <iframe t-att-src="'https://view.officeapps.live.com/op/view.aspx?src=' + encodeURIComponent(props.url)"
|
||||
style="width:100%; height:500px;"
|
||||
frameborder="0">
|
||||
</iframe> -->
|
||||
</t>
|
||||
|
||||
<t t-else="">
|
||||
<p class="text-muted mt-2">
|
||||
This file does not Support Preview, Kindly Download Instead.
|
||||
</p>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/* Make dialog fill most of the screen */
|
||||
.o_dialog_container.o_fullscreen_preview {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
height: 90vh !important;
|
||||
}
|
||||
|
||||
/* Remove padding from body */
|
||||
.o_fullscreen_preview .modal-body {
|
||||
padding: 0 !important;
|
||||
height: calc(90vh - 50px); /* subtract header height */
|
||||
}
|
||||
/* Ensure wrapper fills height */
|
||||
.o_fullscreen_preview_body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Full-size iframe */
|
||||
.o_preview_frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.o_fullscreen_preview iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { BinaryField, ListBinaryField } from "@web/views/fields/binary/binary_field";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { AttachmentPreviewPopup } from "@universal_attachment_preview/attachment_preview_popup/attachment_preview_popup";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
patch(BinaryField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.dialog = useService("dialog");
|
||||
this.rpc = rpc;
|
||||
},
|
||||
|
||||
async onPreviewFile(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const data = this.getDownloadData();
|
||||
if (!data || !data.model || !data.id || !data.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1️⃣ Convert Binary → Attachment (or fetch existing)
|
||||
const result = await this.rpc("/lookup_or_create/attachment", {
|
||||
model: data.model,
|
||||
res_id: data.id,
|
||||
field: data.field,
|
||||
});
|
||||
|
||||
if (!result || !result.attachment_id) {
|
||||
alert("Could not generate preview file.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 📄 Correct file name
|
||||
let filename = data.filename;
|
||||
if (result.name && !/^file($|[_\.])/i.test(result.name)) {
|
||||
filename = result.name;
|
||||
}
|
||||
|
||||
|
||||
// 2️⃣ Open preview dialog with attachment details
|
||||
this.dialog.add(AttachmentPreviewPopup, {
|
||||
attachmentId: result.attachment_id,
|
||||
filename: filename,
|
||||
mimetype: result.mimetype,
|
||||
url: result.url,
|
||||
dialogClass: "o_fullscreen_preview",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Patch list view binary
|
||||
patch(ListBinaryField.prototype, {
|
||||
onPreviewFile(ev) {
|
||||
return BinaryField.prototype.onPreviewFile.call(this, ev);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { BinaryField, ListBinaryField } from "@web/views/fields/binary/binary_field";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
// ---------------------------
|
||||
// PREVIEW COMPONENT
|
||||
// ---------------------------
|
||||
export class PreviewContent extends Component {
|
||||
setup() {
|
||||
console.log("[PreviewContent] Setup called with props:", this.props);
|
||||
// Log the actual props we need
|
||||
console.log("[PreviewContent] URL:", this.props.url);
|
||||
console.log("[PreviewContent] Direct URL:", this.props.directUrl);
|
||||
console.log("[PreviewContent] Absolute URL:", this.getAbsoluteUrl());
|
||||
console.log("[PreviewContent] Filename:", this.props.filename);
|
||||
console.log("[PreviewContent] Mimetype:", this.props.mimetype);
|
||||
}
|
||||
|
||||
static template = owl.xml/* xml */`
|
||||
<div class="preview-container" style="width:100%; height:100%;">
|
||||
<t t-if="isImage">
|
||||
<img t-att-src="props.url" style="width:100%; height:auto; max-height:80vh;"/>
|
||||
</t>
|
||||
|
||||
<t t-if="isPDF">
|
||||
<iframe t-att-src="getAbsoluteUrl()" style="width:100%; height:80vh; border:none;"></iframe>
|
||||
</t>
|
||||
|
||||
<t t-if="isVideo">
|
||||
<video style="width:100%; height:auto; max-height:80vh;" controls="controls">
|
||||
<source t-att-src="props.url"/>
|
||||
</video>
|
||||
</t>
|
||||
|
||||
<t t-if="isDocument">
|
||||
<iframe t-att-src="getGoogleDocsUrl()" style="width:100%; height:80vh; border:none;"></iframe>
|
||||
</t>
|
||||
|
||||
<t t-if="isOther">
|
||||
<div class="text-center p-4">
|
||||
<i class="fa fa-file-o fa-5x text-muted mb-3"></i>
|
||||
<h4>Preview Not Available</h4>
|
||||
<p class="text-muted">This file type cannot be previewed directly.</p>
|
||||
<p class="text-muted">Mimetype: <t t-esc="props.mimetype || 'Unknown'"/></p>
|
||||
<a t-att-href="props.url" download="download" class="btn btn-primary mt-2">
|
||||
<i class="fa fa-download mr-1"/>Download File
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
|
||||
static props = {
|
||||
url: String,
|
||||
directUrl: String,
|
||||
filename: String,
|
||||
mimetype: { type: String, optional: true },
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
mimetype: "",
|
||||
};
|
||||
|
||||
get isImage() {
|
||||
console.log("[PreviewContent] Checking if image, mimetype:", this.props.mimetype);
|
||||
const result = this.props.mimetype && this.props.mimetype.startsWith("image/");
|
||||
console.log("[PreviewContent] isImage result:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
get isPDF() {
|
||||
console.log("[PreviewContent] Checking if PDF, mimetype:", this.props.mimetype);
|
||||
const result = this.props.mimetype === "application/pdf";
|
||||
console.log("[PreviewContent] isPDF result:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
get isVideo() {
|
||||
console.log("[PreviewContent] Checking if video, mimetype:", this.props.mimetype);
|
||||
const result = this.props.mimetype && this.props.mimetype.startsWith("video/");
|
||||
console.log("[PreviewContent] isVideo result:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
get isDocument() {
|
||||
console.log("[PreviewContent] Checking if document, mimetype:", this.props.mimetype);
|
||||
const docTypes = [
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain'
|
||||
];
|
||||
const result = docTypes.includes(this.props.mimetype);
|
||||
console.log("[PreviewContent] isDocument result:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
get isOther() {
|
||||
console.log("[PreviewContent] Checking if other type");
|
||||
const result = !this.isImage && !this.isPDF && !this.isVideo && !this.isDocument;
|
||||
console.log("[PreviewContent] isOther result:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
getAbsoluteUrl() {
|
||||
// Convert relative URL to absolute URL
|
||||
const absoluteUrl = window.location.origin + this.props.directUrl;
|
||||
console.log("[PreviewContent] Absolute URL:", absoluteUrl);
|
||||
return absoluteUrl;
|
||||
}
|
||||
|
||||
getGoogleDocsUrl() {
|
||||
const absoluteUrl = this.getAbsoluteUrl();
|
||||
const url = `https://docs.google.com/viewer?url=${encodeURIComponent(absoluteUrl)}&embedded=true`;
|
||||
console.log("[PreviewContent] Google Docs URL:", url);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// REGISTER COMPONENT
|
||||
console.log("[DEBUG] Registering PreviewContent component");
|
||||
registry.category("components").add("PreviewContent", PreviewContent);
|
||||
console.log("[DEBUG] PreviewContent component registered successfully");
|
||||
|
||||
// Create a wrapper component for the dialog
|
||||
class PreviewDialog extends Component {
|
||||
setup() {
|
||||
console.log("[PreviewDialog] Setup called with props:", this.props);
|
||||
// Extract the actual file props from the dialog options
|
||||
this.fileProps = this.props.props || {};
|
||||
console.log("[PreviewDialog] Extracted file props:", this.fileProps);
|
||||
}
|
||||
|
||||
static template = owl.xml/* xml */`
|
||||
<div class="modal-body" style="padding: 0;">
|
||||
<PreviewContent url="fileProps.url" directUrl="fileProps.directUrl" filename="fileProps.filename" mimetype="fileProps.mimetype"/>
|
||||
</div>
|
||||
`;
|
||||
static components = { PreviewContent };
|
||||
static props = {
|
||||
title: String,
|
||||
props: Object,
|
||||
size: String,
|
||||
close: Function,
|
||||
};
|
||||
}
|
||||
|
||||
// Register the wrapper component
|
||||
console.log("[DEBUG] Registering PreviewDialog component");
|
||||
registry.category("components").add("PreviewDialog", PreviewDialog);
|
||||
console.log("[DEBUG] PreviewDialog component registered successfully");
|
||||
|
||||
// ---------------------------
|
||||
// PATCH BINARY FIELD
|
||||
// ---------------------------
|
||||
console.log("[DEBUG] Patching BinaryField");
|
||||
patch(BinaryField.prototype, {
|
||||
setup() {
|
||||
console.log("[BinaryField] Setup called");
|
||||
super.setup();
|
||||
this.rpc = rpc;
|
||||
this.dialog = useService("dialog");
|
||||
console.log("[BinaryField] Services initialized:", { rpc: !!this.rpc, dialog: !!this.dialog });
|
||||
},
|
||||
|
||||
async onPreviewFile(ev) {
|
||||
console.log("[BinaryField] onPreviewFile triggered");
|
||||
ev.stopPropagation();
|
||||
|
||||
const data = this.getDownloadData();
|
||||
console.log("[BinaryField] Download data:", data);
|
||||
|
||||
// Check if we have the required data
|
||||
if (!data || !data.model || !data.id || !data.field) {
|
||||
console.error("[BinaryField] Missing required data:", data);
|
||||
alert("Error: Missing file information");
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the correct URL for the file content
|
||||
const directUrl = `/web/content?model=${data.model}&id=${data.id}&field=${data.field}&filename=${encodeURIComponent(data.filename || '')}`;
|
||||
console.log("[BinaryField] Direct URL constructed:", directUrl);
|
||||
|
||||
// Create a blob URL from the file content
|
||||
try {
|
||||
console.log("[BinaryField] Fetching file...");
|
||||
const response = await fetch(directUrl);
|
||||
console.log("[BinaryField] Response status:", response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
console.log("[BinaryField] Blob created, size:", blob.size, "type:", blob.type);
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
console.log("[BinaryField] Object URL created:", url);
|
||||
|
||||
// Prepare the props for the PreviewContent component
|
||||
const previewProps = {
|
||||
url, // Blob URL for direct preview in the browser
|
||||
directUrl, // Direct server URL for Google Docs Viewer and PDF
|
||||
filename: data.filename || 'Unknown',
|
||||
mimetype: blob.type || data.mimetype || 'application/octet-stream'
|
||||
};
|
||||
|
||||
console.log("[BinaryField] Preview props:", previewProps);
|
||||
console.log("[BinaryField] About to open dialog...");
|
||||
|
||||
// Use the wrapper component class directly
|
||||
this.dialog.add(PreviewDialog, {
|
||||
title: `Preview: ${data.filename || 'File'}`,
|
||||
props: previewProps,
|
||||
size: "extra-large",
|
||||
technical: false, // Ensure the dialog is visible
|
||||
});
|
||||
console.log("[BinaryField] Dialog opened successfully");
|
||||
} catch (error) {
|
||||
console.error("[BinaryField] Error fetching file:", error);
|
||||
alert("Could not preview the file. Please try downloading it instead.");
|
||||
// Fallback to download
|
||||
console.log("[BinaryField] Falling back to download");
|
||||
window.open(directUrl + '&download=true', '_blank');
|
||||
}
|
||||
},
|
||||
|
||||
_base64ToBlob(base64Data, mimeType) {
|
||||
console.log("[BinaryField] Converting base64 to blob, type:", mimeType);
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteArrays = [];
|
||||
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);
|
||||
}
|
||||
byteArrays.push(new Uint8Array(byteNumbers));
|
||||
}
|
||||
const blob = new Blob(byteArrays, { type: mimeType });
|
||||
console.log("[BinaryField] Blob created from base64, size:", blob.size);
|
||||
return blob;
|
||||
},
|
||||
});
|
||||
|
||||
console.log("[DEBUG] BinaryField patched successfully");
|
||||
|
||||
console.log("[DEBUG] Patching ListBinaryField");
|
||||
patch(ListBinaryField.prototype, {
|
||||
onPreviewFile(ev) {
|
||||
console.log("[ListBinaryField] onPreviewFile triggered, delegating to BinaryField");
|
||||
return BinaryField.prototype.onPreviewFile.call(this, ev);
|
||||
},
|
||||
});
|
||||
console.log("[DEBUG] ListBinaryField patched successfully");
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Inherit BinaryField template -->
|
||||
<t t-name="universal_attachment_preview.BinaryField" t-inherit="web.BinaryField" t-inherit-mode="extension">
|
||||
|
||||
<!-- Insert AFTER the trash icon -->
|
||||
<xpath expr="//button[hasclass('o_clear_file_button')]" position="after">
|
||||
<button
|
||||
class="btn btn-link btn-sm lh-1 fa fa-eye o_preview_file_button"
|
||||
data-tooltip="Preview"
|
||||
aria-label="Preview"
|
||||
t-on-click.prevent="onPreviewFile"
|
||||
></button>
|
||||
</xpath>
|
||||
|
||||
</t>
|
||||
|
||||
<t t-name="universal_attachment_preview.ListBinaryField" t-inherit="web.ListBinaryField" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('o_clear_file_button')]" position="after">
|
||||
<button
|
||||
class="btn btn-link btn-sm lh-1 fa fa-eye o_preview_file_button"
|
||||
data-tooltip="Preview"
|
||||
aria-label="Preview"
|
||||
t-on-click.prevent="onPreviewFile"
|
||||
></button>
|
||||
</xpath>
|
||||
|
||||
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="universal_attachment_preview.PopupPreview">
|
||||
<div class="p-3">
|
||||
<h3>Hello World</h3>
|
||||
<p>Popup loaded successfully!</p>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- PREVIEW POPUP WINDOW -->
|
||||
<t t-name="universal_attachment_preview.FilePreviewDialog">
|
||||
<Dialog title="Preview: [[ props.filename ]]">
|
||||
|
||||
<!-- AUTO HANDLING FOR ANY FILE TYPE -->
|
||||
<t t-if="props.url.endsWith('.pdf')">
|
||||
<iframe t-att-src="props.url"
|
||||
style="width:100%; height:80vh; border:none;" />
|
||||
</t>
|
||||
|
||||
<t t-elif="props.url.match(/\.(jpg|jpeg|png|gif|webp|bmp)$/i)">
|
||||
<img t-att-src="props.url"
|
||||
style="max-width:100%; max-height:80vh;" />
|
||||
</t>
|
||||
|
||||
<t t-elif="props.url.match(/\.(mp4|webm|ogg)$/i)">
|
||||
<video class="o-FileViewer-view w-75 h-75" style="width:100%; height:auto; max-height:80vh;" t-on-click.stop="" controls="controls">
|
||||
<source t-att-src="props.url" />
|
||||
</video>
|
||||
</t>
|
||||
|
||||
<t t-else="">
|
||||
<!-- DEFAULT: Non-previewable types -->
|
||||
<iframe t-att-src="props.url"
|
||||
style="width:100%; height:80vh; border:none;" />
|
||||
<p class="text-muted mt-2">
|
||||
If the file does not preview, it will download automatically.
|
||||
</p>
|
||||
</t>
|
||||
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="universal_attachment_preview.dialog">
|
||||
<div class="modal-body" t-raw="props.content"></div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="ftp_attachment_preview.PreviewModal">
|
||||
<div class="o_preview_container">
|
||||
<t t-if="previewType == 'image'">
|
||||
<img t-att-src="attachment.preview_url" class="img-fluid" alt="Preview"/>
|
||||
</t>
|
||||
<t t-elif="previewType == 'pdf'">
|
||||
<iframe t-att-src="attachment.preview_url" class="w-100" style="height: 70vh;"/>
|
||||
</t>
|
||||
<t t-elif="previewType == 'video'">
|
||||
<video id="preview_video" class="video-js vjs-default-skin" controls="" preload="auto" data-setup='{}'>
|
||||
<source t-att-src="attachment.preview_url" t-att-type="attachment.mimetype"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</t>
|
||||
<t t-elif="previewType == 'text'">
|
||||
<iframe t-att-src="attachment.preview_url" class="w-100" style="height: 70vh;"/>
|
||||
</t>
|
||||
<t t-elif="previewType == 'office'">
|
||||
<iframe t-att-src="attachment.preview_url" class="w-100" style="height: 70vh;"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-warning">
|
||||
Preview not available for this file type.
|
||||
<a t-att-href="attachment.preview_url" class="btn btn-primary">Download</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<!-- <template id="assets_backend" inherit_id="web.assets_backend" name="universal_preview_assets">-->
|
||||
<!-- <xpath expr="." position="inside">-->
|
||||
<!-- <!– assets loaded via manifest assets key –>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- </template>-->
|
||||
|
||||
<template id="universal_attachment_preview_modal">
|
||||
<div class="universal-preview-modal">
|
||||
<t t-if="mimetype and mimetype.startswith('image/')">
|
||||
<img t-att-src="file_url" style="max-width: 100%; max-height: 80vh;" />
|
||||
</t>
|
||||
<t t-elif="mimetype == 'application/pdf'">
|
||||
<iframe t-att-src="file_url" style="width:100%; height:80vh;" frameborder="0"></iframe>
|
||||
</t>
|
||||
<t t-elif="mimetype.startswith('video/')">
|
||||
<video controls="" style="width:100%; height:auto; max-height:80vh;">
|
||||
<source t-att-src="file_url" t-att-type="mimetype"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</t>
|
||||
<t t-elif="mimetype.startswith('audio/')">
|
||||
<audio controls="">
|
||||
<source t-att-src="file_url" t-att-type="mimetype"/>
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
</t>
|
||||
<t t-elif="mimetype and mimetype.startswith('text/')">
|
||||
<iframe t-att-src="file_url" style="width:100%; height:80vh;" frameborder="0"></iframe>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div>
|
||||
<p>Preview not available. <a t-att-href="file_url">Download</a> the file to view it locally.</p>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue