[REF] web, mail: rename attachment_viewer to file_viewer

Since the attachement viewer was moved to core, this renames it to file
viewer to decouple it from the Attachment model/concept from the mail
module.

Part-of: odoo/odoo#119625
This commit is contained in:
Antoine Guenet
2023-05-23 16:31:21 +02:00
parent 8aec394f71
commit 995bf2d883
19 changed files with 189 additions and 175 deletions
@@ -5,7 +5,7 @@ import { Component } from "@odoo/owl";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { useAttachmentViewer } from "@web/core/attachment_viewer/attachment_viewer_hook";
import { useFileViewer } from "@web/core/file_viewer/file_viewer_hook";
import { _t } from "@web/core/l10n/translation";
import { url } from "@web/core/utils/urls";
@@ -25,7 +25,7 @@ export class AttachmentList extends Component {
// Arbitrary high value, this is effectively a max-width.
this.imagesWidth = 1920;
this.dialog = useService("dialog");
this.attachmentViewer = useAttachmentViewer();
this.fileViewer = useFileViewer();
}
/**
@@ -19,7 +19,7 @@
aria-label="View image"
role="menuitem"
t-att-data-mimetype="attachment.mimetype"
t-on-click="() => this.attachmentViewer.open(attachment, props.attachments)"
t-on-click="() => this.fileViewer.open(attachment, props.attachments)"
>
<img
class="img img-fluid my-0 mx-auto o-viewable"
@@ -52,7 +52,7 @@
'o-viewable': attachment.isViewable,
'o-isUploading': attachment.uploading,
}" t-att-title="attachment.name ? attachment.name : undefined" role="menu" t-att-aria-label="attachment.name"
t-on-click="() => this.attachmentViewer.open(attachment, props.attachments)"
t-on-click="() => this.fileViewer.open(attachment, props.attachments)"
>
<!-- o_image from mimetype.scss -->
<div class="o-mail-AttachmentCard-image o_image flex-shrink-0 m-1" role="menuitem" aria-label="Preview" t-att-tabindex="-1" t-att-aria-disabled="false" t-att-data-mimetype="attachment.mimetype"/>
@@ -13,7 +13,7 @@
.arrow {
width: 5%;
@include o-position-absolute(50%, 0);
@include o-AttachmentViewer-arrow;
@include o-FileViewer-arrow;
transition: width 0.3s;
padding-top: 30px;
height: 75px;
@@ -15,7 +15,7 @@ $o-mail-Chatter-minWidth: 530px !default;
$o-mail-Discuss-inspector: 250px !default;
$o-mail-Discuss-headerHeight: 48px !default;
@mixin o-AttachmentViewer-arrow {
@mixin o-FileViewer-arrow {
background-color: rgba(black, 0.4);
color: rgba(map-get($theme-colors, 'light'), 0.7);
+1 -1
View File
@@ -27,7 +27,7 @@
@include o-position-absolute(50%, $grid-gutter-width*0.5);
border-radius: 100%;
padding: 12px 16px 11px 18px;
@include o-AttachmentViewer-arrow;
@include o-FileViewer-arrow;
}
.arrow-left {
@@ -109,12 +109,12 @@ patch(FormCompiler.prototype, "mail", {
if (webClientViewAttachmentViewHookXml) {
// in sheet bg (attachment viewer present)
setAttributes(webClientViewAttachmentViewHookXml, {
"t-if": `__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
"t-if": `__comp__.hasFileViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
});
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
sheetBgChatterContainerHookXml.classList.add("o-isInFormSheetBg");
setAttributes(sheetBgChatterContainerHookXml, {
"t-if": `__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
"t-if": `__comp__.hasFileViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
});
append(formSheetBgXml, sheetBgChatterContainerHookXml);
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(
@@ -128,12 +128,12 @@ patch(FormCompiler.prototype, "mail", {
// after sheet bg (standard position, either aside or below)
if (webClientViewAttachmentViewHookXml) {
setAttributes(chatterContainerHookXml, {
"t-if": `!(__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL})`,
"t-attf-class": `{{ __comp__.uiService.size >= ${SIZES.XXL} and !(__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}) ? "o-aside" : "" }}`,
"t-if": `!(__comp__.hasFileViewer() and __comp__.uiService.size >= ${SIZES.XXL})`,
"t-attf-class": `{{ __comp__.uiService.size >= ${SIZES.XXL} and !(__comp__.hasFileViewer() and __comp__.uiService.size >= ${SIZES.XXL}) ? "o-aside" : "" }}`,
});
setAttributes(chatterContainerXml, {
isInFormSheetBg: "__comp__.hasAttachmentViewer()",
hasMessageListScrollAdjust: `__comp__.uiService.size >= ${SIZES.XXL} and !(__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL})`,
isInFormSheetBg: "__comp__.hasFileViewer()",
hasMessageListScrollAdjust: `__comp__.uiService.size >= ${SIZES.XXL} and !(__comp__.hasFileViewer() and __comp__.uiService.size >= ${SIZES.XXL})`,
});
} else {
setAttributes(chatterContainerXml, {
@@ -33,7 +33,7 @@ patch(FormRenderer.prototype, "mail", {
/**
* @returns {boolean}
*/
hasAttachmentViewer() {
hasFileViewer() {
if (!this.threadService || this.uiService.size < SIZES.XXL || !this.props.record.resId) {
return false;
}
@@ -115,7 +115,7 @@ QUnit.test("view attachment", async (assert) => {
await openDiscuss(channelId);
assert.containsOnce($, ".o-mail-AttachmentImage img");
await click(".o-mail-AttachmentImage");
assert.containsOnce($, ".o-AttachmentViewer");
assert.containsOnce($, ".o-FileViewer");
});
QUnit.test("close attachment viewer", async (assert) => {
@@ -140,10 +140,10 @@ QUnit.test("close attachment viewer", async (assert) => {
assert.containsOnce($, ".o-mail-AttachmentImage img");
await click(".o-mail-AttachmentImage");
assert.containsOnce($, ".o-AttachmentViewer");
assert.containsOnce($, ".o-FileViewer");
await click(".o-AttachmentViewer div[aria-label='Close']");
assert.containsNone($, ".o-AttachmentViewer");
await click(".o-FileViewer div[aria-label='Close']");
assert.containsNone($, ".o-FileViewer");
});
QUnit.test(
@@ -177,8 +177,8 @@ QUnit.test(
const { openDiscuss } = await start();
await openDiscuss(channelId);
await click(".o-mail-AttachmentImage");
const image = $(".o-AttachmentViewer-viewImage")[0];
await click(".o-AttachmentViewer div[aria-label='Close']");
const image = $(".o-FileViewer-viewImage")[0];
await click(".o-FileViewer div[aria-label='Close']");
// Simulate image becoming loaded.
let successfulLoad;
try {
@@ -314,10 +314,10 @@ QUnit.test(
click(".o-mail-AttachmentCard:contains(test.odt)").catch(() => {});
await nextTick();
assert.containsNone($, ".o-AttachmentViewer");
assert.containsNone($, ".o-FileViewer");
await click(".o-mail-AttachmentImage[title='test.png']");
assert.containsOnce($, ".o-AttachmentViewer");
assert.containsOnce($, ".o-FileViewer");
}
);
@@ -136,16 +136,16 @@ QUnit.test("view attachments", async (assert) => {
views: [[false, "form"]],
});
await click('.o-mail-AttachmentCard[aria-label="Blah.txt"] .o-mail-AttachmentCard-image');
assert.containsOnce($, ".o-AttachmentViewer");
assert.containsOnce($, ".o-AttachmentViewer-header:contains(Blah.txt)");
assert.containsOnce($, ".o-AttachmentViewer div[aria-label='Next']");
assert.containsOnce($, ".o-FileViewer");
assert.containsOnce($, ".o-FileViewer-header:contains(Blah.txt)");
assert.containsOnce($, ".o-FileViewer div[aria-label='Next']");
await click(".o-AttachmentViewer div[aria-label='Next']");
assert.containsOnce($, ".o-AttachmentViewer-header:contains(Blu.txt)");
assert.containsOnce($, ".o-AttachmentViewer div[aria-label='Next']");
await click(".o-FileViewer div[aria-label='Next']");
assert.containsOnce($, ".o-FileViewer-header:contains(Blu.txt)");
assert.containsOnce($, ".o-FileViewer div[aria-label='Next']");
await click(".o-AttachmentViewer div[aria-label='Next']");
assert.containsOnce($, ".o-AttachmentViewer-header:contains(Blah.txt)");
await click(".o-FileViewer div[aria-label='Next']");
assert.containsOnce($, ".o-FileViewer-header:contains(Blah.txt)");
});
QUnit.test("scroll to attachment box when toggling on", async (assert) => {
@@ -2,13 +2,13 @@
import { CANCEL_GLOBAL_CLICK, KanbanRecord } from "@web/views/kanban/kanban_record";
import { useService } from "@web/core/utils/hooks";
import { useAttachmentViewer } from "@web/core/attachment_viewer/attachment_viewer_hook";
import { useFileViewer } from "@web/core/file_viewer/file_viewer_hook";
export class MrpDocumentsKanbanRecord extends KanbanRecord {
setup() {
super.setup();
this.attachmentService = useService("mail.attachment");
this.attachmentViewer = useAttachmentViewer();
this.fileViewer = useFileViewer();
}
/**
* @override
@@ -29,7 +29,7 @@ export class MrpDocumentsKanbanRecord extends KanbanRecord {
name: this.props.record.data.name,
mimetype: this.props.record.data.mimetype,
});
this.attachmentViewer.open(attachment)
this.fileViewer.open(attachment)
return;
}
return super.onGlobalClick(...arguments);
@@ -231,12 +231,12 @@ QUnit.module('MrpDocumentsKanbanView', {
assert.containsOnce(target, ".o_kanban_previewer");
await click(target.querySelector(".o_kanban_previewer"));
await waitUntil('.o-AttachmentViewer');
assert.containsOnce(target, '.o-AttachmentViewer');
assert.containsOnce(target, '.o-AttachmentViewer-headerButton .fa-times');
await waitUntil('.o-FileViewer');
assert.containsOnce(target, '.o-FileViewer');
assert.containsOnce(target, '.o-FileViewer-headerButton .fa-times');
await click(target, '.o-AttachmentViewer-headerButton .fa-times');
assert.containsNone(target, '.o-AttachmentViewer');
await click(target, '.o-FileViewer-headerButton .fa-times');
assert.containsNone(target, '.o-FileViewer');
});
});
+3 -3
View File
@@ -185,7 +185,7 @@ This module provides the core of the Odoo Web Client.
('remove', 'web/static/src/webclient/navbar/navbar.scss'), # already in assets_common
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
('remove', 'web/static/src/views/form/button_box/*.scss'),
('remove', 'web/static/src/core/attachment_viewer/attachment_viewer.dark.scss'),
('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'),
# remove the report code and whitelist only what's needed
('remove', 'web/static/src/webclient/actions/reports/**/*'),
@@ -400,7 +400,7 @@ This module provides the core of the Odoo Web Client.
'web/static/src/core/**/*',
('remove', 'web/static/src/core/commands/**/*'),
('remove', 'web/static/src/core/debug/debug_menu.js'),
('remove', 'web/static/src/core/attachment_viewer/attachment_viewer.dark.scss'),
('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'),
'web/static/src/public/error_notifications.js',
'web/static/src/legacy/utils.js',
@@ -483,7 +483,7 @@ This module provides the core of the Odoo Web Client.
# ---------------------------------------------------------------------
"web.dark_mode_assets_common": [
('include', 'web.assets_common'),
'web/static/src/core/attachment_viewer/attachment_viewer.dark.scss',
'web/static/src/core/file_viewer/file_viewer.dark.scss',
],
"web.dark_mode_assets_backend": [
('include', 'web.assets_backend'),
@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.AttachmentViewer" owl="1">
<div class="d-flex justify-content-center" t-att-class="{ 'modal fixed-top bottom-0': props.modal }">
<div class="o-AttachmentViewer flex-column align-items-center d-flex w-100 h-100" tabindex="0">
<div class="o-AttachmentViewer-header position-absolute top-0 d-flex w-100 bg-black-75 text-400">
<div t-if="isViewable" class="d-flex align-items-center ms-4 me-2">
<i t-if="state.attachment.isImage" class="fa fa-picture-o" role="img" title="Image"/>
<i t-if="state.attachment.isPdf" class="fa fa-file-text" role="img" title="PDF file"/>
<i t-if="state.attachment.isText" class="fa fa-file-text" role="img" title="Text file"/>
<i t-if="state.attachment.isVideo" class="fa fa-video-camera" role="img" title="Video"/>
</div>
<div class="d-flex align-items-center mx-2 overflow-auto">
<span class="text-truncate" t-out="state.attachment.displayName"/>
</div>
<div class="flex-grow-1"/>
<div class="o-AttachmentViewer-download o-AttachmentViewer-headerButton d-flex align-items-center px-3 cursor-pointer" role="button" title="Download">
<a t-att-href="state.attachment.downloadUrl" class="text-reset" download="">
<i class="fa fa-download fa-fw" role="img"/>
<span>Download</span>
</a>
</div>
<div t-on-click.stop="close" class="o-AttachmentViewer-headerButton d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer" role="button" title="Close (Esc)" aria-label="Close">
<i class="fa fa-fw fa-times" role="img"/>
</div>
</div>
<div t-on-click.stop="close" t-on-mousemove="onMousemoveView" class="o-AttachmentViewer-main position-absolute top-0 bottom-0 start-0 end-0 align-items-center justify-content-center d-flex" t-att-class="{ 'o_with_img overflow-hidden': state.attachment.isImage }">
<div t-if="state.attachment.isImage" class="o-AttachmentViewer-zoomer position-absolute align-items-center justify-content-center d-flex w-100 h-100" t-ref="zoomer">
<div t-if="!state.imageLoaded" class="position-absolute">
<i class="fa fa-3x fa-circle-o-notch fa-fw fa-spin text-white" role="img" title="Loading"/>
</div>
<img t-on-click.stop="" t-on-load="onImageLoaded" t-on-wheel="onWheelImage" t-on-mousedown.stop="onMousedownImage" t-on-mouseup.stop="onMouseupImage" class="o-AttachmentViewer-view o-AttachmentViewer-viewImage mw-100 mh-100 transition-base" t-att-src="state.attachment.defaultSource" t-att-style="imageStyle" draggable="false" alt="Viewer" t-ref="image"/>
</div>
<iframe t-if="state.attachment.isPdf" class="o-AttachmentViewer-view w-75 h-100 border-0" t-ref="iframeViewerPdf" t-att-class="{ 'w-100': ui.isSmall }" t-att-src="state.attachment.defaultSource"/>
<iframe t-if="state.attachment.isText" class="o-AttachmentViewer-view o-isText o_text w-75 h-100 border-0" t-att-src="state.attachment.defaultSource"/>
<iframe t-if="state.attachment.isUrlYoutube" allow="autoplay; encrypted-media" class="o-AttachmentViewer-view w-75 h-100 border-0" t-att-src="state.attachment.defaultSource" height="315" width="560"/>
<video t-if="state.attachment.isVideo" class="o-AttachmentViewer-view w-75 h-75" t-att-class="{ 'w-100 h-100': ui.isSmall }" t-on-click.stop="" controls="controls">
<source t-att-data-type="state.attachment.mimetype" t-att-src="state.attachment.defaultSource"/>
</video>
</div>
<div t-if="state.attachment.isImage" class="position-absolute bottom-0 d-flex" role="toolbar">
<div class="o-AttachmentViewer-toolbarButton p-3 rounded-0" t-on-click="zoomIn" title="Zoom In (+)" role="button">
<i class="fa fa-fw fa-plus" role="img"/>
</div>
<div class="o-AttachmentViewer-toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': state.scale === 1 }" t-on-click="resetZoom" role="button" title="Reset Zoom (0)">
<i class="fa fa-fw fa-search" role="img"/>
</div>
<div class="o-AttachmentViewer-toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': state.scale === minScale }" t-on-click="zoomOut" title="Zoom Out (-)" role="button">
<i class="fa fa-fw fa-minus" role="img"/>
</div>
<div class="o-AttachmentViewer-toolbarButton p-3 rounded-0" t-on-click="rotate" title="Rotate (r)" role="button">
<i class="fa fa-fw fa-repeat" role="img"/>
</div>
<div class="o-AttachmentViewer-toolbarButton p-3 rounded-0" t-on-click="onClickPrint" title="Print" role="button">
<i class="fa fa-fw fa-print" role="img"/>
</div>
<div class="o-AttachmentViewer-download o-AttachmentViewer-toolbarButton p-3 rounded-0 cursor-pointer" title="Download" role="button">
<a t-att-href="state.attachment.downloadUrl" class="text-reset" download="">
<i class="fa fa-download fa-fw" role="img"/>
</a>
</div>
</div>
<t t-if="props.attachments.length > 1">
<div class="o-AttachmentViewer-navigation position-absolute top-0 bottom-0 start-0 align-items-center justify-content-center d-flex my-auto ms-3 rounded-circle" t-on-click="previous" title="Previous (Left-Arrow)" aria-label="Previous" role="button">
<span class="oi oi-chevron-left" role="img"/>
</div>
<div class="o-AttachmentViewer-navigation position-absolute top-0 bottom-0 end-0 align-items-center justify-content-center d-flex my-auto me-3 rounded-circle" t-on-click="next" title="Next (Right-Arrow)" aria-label="Next" role="button">
<span class="oi oi-chevron-right" role="img"/>
</div>
</t>
</div>
</div>
</t>
</templates>
@@ -1,34 +0,0 @@
/* @odoo-module */
import { onWillDestroy } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { AttachmentViewer } from "./attachment_viewer";
let id = 1;
export function useAttachmentViewer() {
const attachmentViewerId = `web.attachment_viewer${id++}`;
/**
* @param {import("@mail/attachments/attachment_model").Attachment} attachment
* @param {import("@mail/attachments/attachment_model").Attachment[]} attachments
*/
function open(attachment, attachments = [attachment]) {
if (!attachment.isViewable) {
return;
}
if (attachments.length > 0) {
const viewableAttachments = attachments.filter((attachment) => attachment.isViewable);
const index = viewableAttachments.indexOf(attachment);
registry.category("main_components").add(attachmentViewerId, {
Component: AttachmentViewer,
props: { attachments: viewableAttachments, startIndex: index, close },
});
}
}
function close() {
registry.category("main_components").remove(attachmentViewerId);
}
onWillDestroy(close);
return { open, close };
}
@@ -2,6 +2,6 @@
// ============================================================================
// No CSS hacks, variables overrides only
.o-AttachmentViewer-toolbarButton {
--web-attachment-viewer-toolbarButton-background-color: #{$o-gray-200};
.o-FileViewer-toolbarButton {
--file-viewer-toolbarButton-background-color: #{$o-gray-200};
}
@@ -4,14 +4,28 @@ import { Component, useExternalListener, useRef, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
/**
* @typedef {Object} File
* @property {string} displayName
* @property {string} downloadUrl
* @property {boolean} [isImage]
* @property {boolean} [isPdf]
* @property {boolean} [isVideo]
* @property {boolean} [isText]
* @property {string} [defaultSource]
* @property {boolean} [isUrlYoutube]
* @property {string} [mimetype]
* @property {boolean} [isViewable]
* @typedef {Object} Props
* @property {Array<T>} attachments
* @property {Array<File>} files
* @property {number} startIndex
* @property {function} close
* @property {boolean} [modal]
* @extends {Component<Props, Env>}
*/
export class AttachmentViewer extends Component {
static template = "web.AttachmentViewer";
export class FileViewer extends Component {
static template = "web.FileViewer";
static components = {};
static props = ["attachments", "startIndex", "close", "modal?"];
static props = ["files", "startIndex", "close", "modal?"];
static defaultProps = {
modal: true,
};
@@ -37,7 +51,7 @@ export class AttachmentViewer extends Component {
useExternalListener(document, "keydown", this.onKeydown);
this.state = useState({
index: this.props.startIndex,
attachment: this.props.attachments[this.props.startIndex],
file: this.props.files[this.props.startIndex],
imageLoaded: false,
scale: 1,
angle: 0,
@@ -54,18 +68,18 @@ export class AttachmentViewer extends Component {
}
next() {
const last = this.props.attachments.length - 1;
this.activateAttachment(this.state.index === last ? 0 : this.state.index + 1);
const last = this.props.files.length - 1;
this.activateFile(this.state.index === last ? 0 : this.state.index + 1);
}
previous() {
const last = this.props.attachments.length - 1;
this.activateAttachment(this.state.index === 0 ? last : this.state.index - 1);
const last = this.props.files.length - 1;
this.activateFile(this.state.index === 0 ? last : this.state.index - 1);
}
activateAttachment(index) {
activateFile(index) {
this.state.index = index;
this.state.attachment = this.props.attachments[index];
this.state.file = this.props.files[index];
}
onKeydown(ev) {
@@ -228,7 +242,7 @@ export class AttachmentViewer extends Component {
</script>
</head>
<body onload='onloadImage()'>
<img src="${this.state.attachment.defaultSource}" alt=""/>
<img src="${this.state.file.defaultSource}" alt=""/>
</body>
</html>`);
printWindow.document.close();
@@ -1,33 +1,33 @@
.o-AttachmentViewer {
.o-FileViewer {
z-index: -1;
outline: none;
}
.o-AttachmentViewer-navigation {
.o-FileViewer-navigation {
width: 40px;
height: 40px;
}
.o-AttachmentViewer-header {
.o-FileViewer-header {
height: $o-navbar-height;
}
.o-AttachmentViewer-main {
.o-FileViewer-main {
z-index: -1;
padding: ($o-navbar-height * 1.125) 0;
}
.o-AttachmentViewer-zoomer {
.o-FileViewer-zoomer {
padding: ($o-navbar-height * 1.125) 0;
}
.o-AttachmentViewer-headerButton:hover {
.o-FileViewer-headerButton:hover {
background-color: rgba($white, 0.1);
color: lighten($gray-400, 15%);
}
.o-AttachmentViewer-toolbarButton {
background-color: var(--web-attachment-viewer-toolbarButton-background-color, #{$o-gray-800});
.o-FileViewer-toolbarButton {
background-color: var(--file-viewer-toolbarButton-background-color, #{$o-gray-800});
color: #fff;
&:hover {
@@ -35,7 +35,7 @@
}
}
.o-AttachmentViewer-view {
.o-FileViewer-view {
background-color: #000000;
box-shadow: 0 0 40px #000000;
outline: none;
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.FileViewer" owl="1">
<div class="d-flex justify-content-center" t-att-class="{ 'modal fixed-top bottom-0': props.modal }">
<div class="o-FileViewer flex-column align-items-center d-flex w-100 h-100" tabindex="0">
<div class="o-FileViewer-header position-absolute top-0 d-flex w-100 bg-black-75 text-400">
<div t-if="isViewable" class="d-flex align-items-center ms-4 me-2">
<i t-if="state.file.isImage" class="fa fa-picture-o" role="img" title="Image"/>
<i t-if="state.file.isPdf" class="fa fa-file-text" role="img" title="PDF file"/>
<i t-if="state.file.isText" class="fa fa-file-text" role="img" title="Text file"/>
<i t-if="state.file.isVideo" class="fa fa-video-camera" role="img" title="Video"/>
</div>
<div class="d-flex align-items-center mx-2 overflow-auto">
<span class="text-truncate" t-out="state.file.displayName"/>
</div>
<div class="flex-grow-1"/>
<div class="o-FileViewer-download o-FileViewer-headerButton d-flex align-items-center px-3 cursor-pointer" role="button" title="Download">
<a t-att-href="state.file.downloadUrl" class="text-reset" download="">
<i class="fa fa-download fa-fw" role="img"/>
<span>Download</span>
</a>
</div>
<div t-on-click.stop="close" class="o-FileViewer-headerButton d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer" role="button" title="Close (Esc)" aria-label="Close">
<i class="fa fa-fw fa-times" role="img"/>
</div>
</div>
<div t-on-click.stop="close" t-on-mousemove="onMousemoveView" class="o-FileViewer-main position-absolute top-0 bottom-0 start-0 end-0 align-items-center justify-content-center d-flex" t-att-class="{ 'o_with_img overflow-hidden': state.file.isImage }">
<div t-if="state.file.isImage" class="o-FileViewer-zoomer position-absolute align-items-center justify-content-center d-flex w-100 h-100" t-ref="zoomer">
<div t-if="!state.imageLoaded" class="position-absolute">
<i class="fa fa-3x fa-circle-o-notch fa-fw fa-spin text-white" role="img" title="Loading"/>
</div>
<img t-on-click.stop="" t-on-load="onImageLoaded" t-on-wheel="onWheelImage" t-on-mousedown.stop="onMousedownImage" t-on-mouseup.stop="onMouseupImage" class="o-FileViewer-view o-FileViewer-viewImage mw-100 mh-100 transition-base" t-att-src="state.file.defaultSource" t-att-style="imageStyle" draggable="false" alt="Viewer" t-ref="image"/>
</div>
<iframe t-if="state.file.isPdf" class="o-FileViewer-view w-75 h-100 border-0" t-ref="iframeViewerPdf" t-att-class="{ 'w-100': ui.isSmall }" t-att-src="state.file.defaultSource"/>
<iframe t-if="state.file.isText" class="o-FileViewer-view o-isText o_text w-75 h-100 border-0" t-att-src="state.file.defaultSource"/>
<iframe t-if="state.file.isUrlYoutube" allow="autoplay; encrypted-media" class="o-FileViewer-view w-75 h-100 border-0" t-att-src="state.file.defaultSource" height="315" width="560"/>
<video t-if="state.file.isVideo" class="o-FileViewer-view w-75 h-75" t-att-class="{ 'w-100 h-100': ui.isSmall }" t-on-click.stop="" controls="controls">
<source t-att-data-type="state.file.mimetype" t-att-src="state.file.defaultSource"/>
</video>
</div>
<div t-if="state.file.isImage" class="position-absolute bottom-0 d-flex" role="toolbar">
<div class="o-FileViewer-toolbarButton p-3 rounded-0" t-on-click="zoomIn" title="Zoom In (+)" role="button">
<i class="fa fa-fw fa-plus" role="img"/>
</div>
<div class="o-FileViewer-toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': state.scale === 1 }" t-on-click="resetZoom" role="button" title="Reset Zoom (0)">
<i class="fa fa-fw fa-search" role="img"/>
</div>
<div class="o-FileViewer-toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': state.scale === minScale }" t-on-click="zoomOut" title="Zoom Out (-)" role="button">
<i class="fa fa-fw fa-minus" role="img"/>
</div>
<div class="o-FileViewer-toolbarButton p-3 rounded-0" t-on-click="rotate" title="Rotate (r)" role="button">
<i class="fa fa-fw fa-repeat" role="img"/>
</div>
<div class="o-FileViewer-toolbarButton p-3 rounded-0" t-on-click="onClickPrint" title="Print" role="button">
<i class="fa fa-fw fa-print" role="img"/>
</div>
<div class="o-FileViewer-download o-FileViewer-toolbarButton p-3 rounded-0 cursor-pointer" title="Download" role="button">
<a t-att-href="state.file.downloadUrl" class="text-reset" download="">
<i class="fa fa-download fa-fw" role="img"/>
</a>
</div>
</div>
<t t-if="props.files.length > 1">
<div class="o-FileViewer-navigation position-absolute top-0 bottom-0 start-0 align-items-center justify-content-center d-flex my-auto ms-3 rounded-circle" t-on-click="previous" title="Previous (Left-Arrow)" aria-label="Previous" role="button">
<span class="oi oi-chevron-left" role="img"/>
</div>
<div class="o-FileViewer-navigation position-absolute top-0 bottom-0 end-0 align-items-center justify-content-center d-flex my-auto me-3 rounded-circle" t-on-click="next" title="Next (Right-Arrow)" aria-label="Next" role="button">
<span class="oi oi-chevron-right" role="img"/>
</div>
</t>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,34 @@
/* @odoo-module */
import { onWillDestroy } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { FileViewer } from "./file_viewer";
let id = 1;
export function useFileViewer() {
const fileViewerId = `web.file_viewer${id++}`;
/**
* @param {import("@web/core/file_viewer/file_viewer").FileViewer.props.files[]} file
* @param {import("@web/core/file_viewer/file_viewer").FileViewer.props.files} files
*/
function open(file, files = [file]) {
if (!file.isViewable) {
return;
}
if (files.length > 0) {
const viewableFiles = files.filter((file) => file.isViewable);
const index = viewableFiles.indexOf(file);
registry.category("main_components").add(fileViewerId, {
Component: FileViewer,
props: { files: viewableFiles, startIndex: index, close },
});
}
}
function close() {
registry.category("main_components").remove(fileViewerId);
}
onWillDestroy(close);
return { open, close };
}