Skip to content

Commit

Permalink
Improve FileViewer components structure & code reusability
Browse files Browse the repository at this point in the history
- Create a FileViewerWrapper component to wrap the FileViewer and its action bar
- Isolate the FileViewerActionBar as a common component
- Update all the viewers component to use the new structure
- Improve all viewers UI/UX
- Add translation for 'opening file'
  • Loading branch information
NicoTuxx committed Nov 22, 2024
1 parent d965f3d commit 33b6167
Show file tree
Hide file tree
Showing 17 changed files with 360 additions and 202 deletions.
39 changes: 39 additions & 0 deletions client/src/components/viewers/FileViewerActionBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->

<template>
<ms-action-bar class="file-viewer-action-bar">
<ms-action-bar-button
v-for="(action, key) in actions"
:key="key"
:icon="action.icon"
@click="action.handler"
:button-label="action.text"
/>
</ms-action-bar>
</template>

<script setup lang="ts">
import { MsActionBar, MsActionBarButton, Translatable } from 'megashark-lib';

// TODO: add a disabled state to the button (ex: max zoom level).
// TODO: extract this interface to a separate file.
interface FileViewerAction {
icon?: string;
text?: Translatable;
handler: () => void;
}

defineProps<{
actions: Array<FileViewerAction>;
}>();
</script>

<style scoped>
.file-viewer-action-bar {
padding: 0.5rem 1rem;
width: fit-content;
background: var(--parsec-color-light-primary-100);
border-radius: 5em;
box-shadow: var(--parsec-shadow-light);
}
</style>
5 changes: 5 additions & 0 deletions client/src/components/viewers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

import FileViewerActionBar from '@/components/viewers/FileViewerActionBar.vue';

export { FileViewerActionBar };
3 changes: 2 additions & 1 deletion client/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,7 @@
"date": "Preview date:"
},
"fileViewers": {
"openWithDefault": "Open with default application"
"openWithDefault": "Open with default application",
"openingFile": "Opening file..."
}
}
3 changes: 2 additions & 1 deletion client/src/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,7 @@
"date": "Date de prévisualisation :"
},
"fileViewers": {
"openWithDefault": "Ouvrir avec l'application par défaut"
"openWithDefault": "Ouvrir avec l'application par défaut",
"openingFile": "Ouverture du fichier..."
}
}
30 changes: 13 additions & 17 deletions client/src/theme/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,19 @@ ion-title {
}

// specific style for the file viewers
.file-viewer {
&-bottombar {
.action-bar {
.ms-action-bar-button {
height: 3em;
width: 3em;
border-radius: 100%;
color: var(--parsec-color-light-primary-600);
opacity: 0.6;
scale: 1;
transition: all 0.2s ease-in-out;

&:hover {
scale: 1.1;
opacity: 1;
}
}
.file-viewer-action-bar {
.ms-action-bar-button {
height: 3em;
width: fit-content;
border-radius: 100%;
color: var(--parsec-color-light-primary-600);
opacity: 0.6;
scale: 1;
transition: all 0.2s ease-in-out;

&:hover {
scale: 1.1;
opacity: 1;
}
}
}
2 changes: 1 addition & 1 deletion client/src/views/files/FoldersPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ async function openEntries(entries: EntryModel[]): Promise<void> {
return;
}

const modal = await openSpinnerModal('OPENING FILE');
const modal = await openSpinnerModal('fileViewers.openingFile');
const contentType = await detectFileContentType(workspaceHandle, entry.path);

try {
Expand Down
64 changes: 59 additions & 5 deletions client/src/views/viewers/AudioViewer.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,80 @@
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->

<template>
<audio
controls
v-if="src.length"
:src="src"
/>
<file-viewer-wrapper>
<template #viewer>
<audio
controls
v-if="src.length"
ref="audioElement"
@play="updateMediaData"
@playing="updateMediaData"
@canplay="updateMediaData"
@pause="updateMediaData"
@volumechange="updateMediaData"
:src="src"
/>
</template>
<!-- Disabled till we add an illustration in the viewer -->
<!-- <template #controls>
<file-viewer-action-bar
:actions="[
{ icon: paused ? play : pause, handler: togglePlayback },
{ icon: getVolumeIcon(), handler: toggleVolume },
]"
/>
</template> -->
</file-viewer-wrapper>
</template>

<script setup lang="ts">
// import { play, pause, volumeHigh, volumeLow, volumeMedium, volumeMute } from 'ionicons/icons';
import { onMounted, ref } from 'vue';
import { FileViewerWrapper } from '@/views/viewers';
import { FileContentInfo } from '@/views/viewers/utils';

const props = defineProps<{
contentInfo: FileContentInfo;
}>();

// const VOLUME_LEVELS = [0, 0.25, 0.5, 1];

const src = ref('');
const audioElement = ref();
const paused = ref(true);
const volume = ref(1);

onMounted(async () => {
src.value = URL.createObjectURL(new Blob([props.contentInfo.data], { type: props.contentInfo.mimeType }));
});

// function togglePlayback(): void {
// audioElement.value.paused ? audioElement.value.play() : audioElement.value.pause();
// }

// function toggleVolume(): void {
// audioElement.value.volume = VOLUME_LEVELS[(VOLUME_LEVELS.indexOf(audioElement.value.volume) + 1) % VOLUME_LEVELS.length];
// }

function updateMediaData(event: Event): void {
volume.value = (event.target as HTMLAudioElement).volume;
paused.value = (event.target as HTMLAudioElement).paused;
}

// function getVolumeIcon(): string {
// switch (volume.value) {
// case 0:
// return volumeMute;
// case 0.25:
// return volumeLow;
// case 0.5:
// return volumeMedium;
// case 1:
// return volumeHigh;
// default:
// return volumeMute;
// }
// }
</script>

<style scoped lang="scss"></style>
17 changes: 11 additions & 6 deletions client/src/views/viewers/DocumentViewer.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->

<template>
<ms-spinner v-show="loading" />
<div
v-show="!loading"
class="document-content"
v-html="htmlContent"
/>
<file-viewer-wrapper>
<template #viewer>
<ms-spinner v-show="loading" />
<div
v-show="!loading"
class="document-content"
v-html="htmlContent"
/>
</template>
</file-viewer-wrapper>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { FileViewerWrapper } from '@/views/viewers';
import { FileContentInfo } from '@/views/viewers/utils';
import mammoth from 'mammoth';
import { MsSpinner } from 'megashark-lib';
Expand Down
85 changes: 9 additions & 76 deletions client/src/views/viewers/FileViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,42 +32,11 @@
</ion-buttons>
</div>

<!-- file-viewer wrapper -->
<div class="file-viewer-wrapper">
<component
:is="viewerComponent"
:content-info="contentInfo"
:config="viewerConfig"
/>
</div>

<!-- file-viewer bottombar -->
<div class="file-viewer-bottombar">
<!-- Here the idea is to put the specific file viewer bottom bar actions -->
<ms-action-bar
v-if="detectedFileType?.type === FileContentType.Image"
class="action-bar"
>
<ms-action-bar-button
id="button-image-zoom-out"
v-show="detectedFileType?.type === FileContentType.Image"
:icon="remove"
@click="zoomOut"
/>
<ms-action-bar-button
id="button-image-zoom-reset"
v-show="detectedFileType?.type === FileContentType.Image"
:icon="resize"
@click="resetZoom"
/>
<ms-action-bar-button
id="button-image-zoom-in"
v-show="detectedFileType?.type === FileContentType.Image"
:icon="add"
@click="zoomIn"
/>
</ms-action-bar>
</div>
<!-- file-viewer component -->
<component
:is="viewerComponent"
:content-info="contentInfo"
/>
</div>
</div>
</ion-content>
Expand All @@ -91,14 +60,15 @@ import {
closeHistoryFile,
} from '@/parsec';
import { IonPage, IonContent, IonButton, IonText, IonIcon, IonButtons } from '@ionic/vue';
import { add, open, remove, resize } from 'ionicons/icons';
import { Base64, MsSpinner, MsImage, MsActionBar, MsActionBarButton } from 'megashark-lib';
import { open } from 'ionicons/icons';
import { Base64, MsSpinner, MsImage } from 'megashark-lib';
import { ref, Ref, type Component, inject, onMounted, shallowRef } from 'vue';
// eslint-disable-next-line max-len
import { ImageViewer, VideoViewer, SpreadsheetViewer, DocumentViewer, AudioViewer, TextViewer, PdfViewer } from '@/views/viewers';
import { Information, InformationLevel, InformationManager, InformationManagerKey, PresentationMode } from '@/services/informationManager';
import { getCurrentRouteQuery, getDocumentPath, getWorkspaceHandle, navigateTo, Routes } from '@/router';
import { DetectedFileType, FileContentType } from '@/common/fileTypes';
import { FileContentInfo, ViewerConfig, imageViewerUtils } from '@/views/viewers/utils';
import { FileContentInfo } from '@/views/viewers/utils';
import { Env } from '@/services/environment';
import { DateTime } from 'luxon';
import { getFileIcon } from '@/common/file';
Expand All @@ -110,9 +80,6 @@ const detectedFileType = ref<DetectedFileType | null>(null);
const loaded = ref(false);
const READ_CHUNK_SIZE = 512_000;
const atDateTime: Ref<DateTime | null> = ref(null);
const viewerConfig: Ref<ViewerConfig> = ref({
zoomLevel: 100,
});

onMounted(async () => {
loaded.value = false;
Expand Down Expand Up @@ -274,18 +241,6 @@ async function onClick(event: MouseEvent): Promise<void> {
}
}
}

function zoomOut(): void {
viewerConfig.value = imageViewerUtils.zoomOut(viewerConfig.value);
}

function resetZoom(): void {
viewerConfig.value = imageViewerUtils.resetZoom(viewerConfig.value);
}

function zoomIn(): void {
viewerConfig.value = imageViewerUtils.zoomIn(viewerConfig.value);
}
</script>

<style scoped lang="scss">
Expand Down Expand Up @@ -357,28 +312,6 @@ function zoomIn(): void {
}
}
}

&-wrapper {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}

&-bottombar {
display: flex;
justify-content: center;
align-items: center;
gap: 1.5rem;

.action-bar {
padding: 0.5rem 1rem;
width: fit-content;
background: var(--parsec-color-light-primary-100);
border-radius: 5em;
box-shadow: var(--parsec-shadow-light);
}
}
}
}
</style>
35 changes: 35 additions & 0 deletions client/src/views/viewers/FileViewerWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->

<template>
<!-- file-viewer wrapper -->
<div class="file-viewer-wrapper">
<slot name="viewer" />
</div>

<!-- file-viewer bottombar -->
<div class="file-viewer-bottombar">
<slot name="controls" />
</div>
</template>

<script setup lang="ts"></script>

<style scoped lang="scss">
.file-viewer-wrapper {
height: 100%;
height: -webkit-fill-available;
height: -moz-available;
height: stretch;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}

.file-viewer-bottombar {
display: flex;
justify-content: center;
align-items: center;
gap: 1.5rem;
}
</style>
Loading

0 comments on commit 33b6167

Please sign in to comment.