Skip to content

Commit

Permalink
Add support for non-dav files
Browse files Browse the repository at this point in the history
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
  • Loading branch information
ChristophWurst authored and nextcloud-command committed Aug 23, 2022
1 parent 5394e9e commit a05dbf3
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 92 deletions.
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ use OCP\IRequest;

class PageController extends Controller {
protected $appName;

/** @var IEventDispatcher */
private $eventDispatcher;

public function __construct($appName,
IRequest $request,
IEventDispatcher $eventDispatcher) {
parent::__construct($appName, $request);

$this->appName = $appName;
$this->eventDispatcher = $eventDispatcher;
}
Expand All @@ -61,11 +61,11 @@ class PageController extends Controller {
This will load all the necessary scripts and make the Viewer accessible trough javascript at `OCA.Viewer`

### Open a file
1. Open a file and let the viewer fetch the folder data
1. Open a file on WebDAV and let the viewer fetch the folder data
```js
OCA.Viewer.open({path: '/path/to/file.jpg'})
```
2. Open a file and profide a list of files
2. Open a file on WebDAV and provide a list of files
```js
OCA.Viewer.open({
path: '/path/to/file.jpg',
Expand All @@ -78,8 +78,46 @@ This will load all the necessary scripts and make the Viewer accessible trough j
...
],
})
// Alternative: pass known file info so it doesn't need to be fetched
const fileInfo = {
filename: '/path/to/file.jpg',
basename: 'file.jpg',
mime: 'image/jpeg',
etag: 'xyz987',
hasPreview: true,
fileid: 13579,
}
OCA.Viewer.open({
fileinfo: fileInfo,
list: [fileInfo],
})
```
The list parameter requires an array of fileinfo. You can check how we generate a fileinfo object [here](https://github.com/nextcloud/viewer/blob/master/src/utils/fileUtils.js#L97) from a dav PROPFIND request data. There is currently no dedicated package for it, but this is coming. You can check the [photos](https://github.com/nextcloud/photos) repository where we also use it.
3. Open a file from an app's route
```js
const fileInfo1 = {
filename: 'https://next.cloud/apps/pizza/topping/pineapple.jpg',
basename: 'pineapple.jpg',
source: 'https://next.cloud/apps/pizza/topping/pineapple.jpg',
mime: 'image/jpeg',
etag: 'abc123',
hasPreview: false,
fileid: 12345,
}
const fileInfo2 = {
filename: 'https://next.cloud/apps/pizza/topping/garlic.jpg',
basename: 'garlic.jpg',
source: 'https://next.cloud/apps/pizza/topping/garlic.jpg',
mime: 'image/jpeg',
etag: 'def456',
hasPreview: false,
fileid: 67890,
}
OCA.Viewer.open({
fileInfo: fileInfo1,
list: [fileInfo1, fileInfo2],
})
```
The list parameter requires an array of fileinfo. You can check how we generate a fileinfo object [here](https://github.com/nextcloud/viewer/blob/master/src/utils/fileUtils.js#L97) from a dav PROPFIND request data. There is currently no dedicated package for it, but this is coming. You can check the [photos](https://github.com/nextcloud/photos) repository where we also uses it.
### Close the viewer
```js
Expand Down
76 changes: 76 additions & 0 deletions cypress/e2e/non-dav-files.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { randHash } from '../utils/'
const randUser = randHash()

describe('Open non-dav files in viewer', function() {
before(function() {
// Init user
cy.nextcloudCreateUser(randUser, 'password')
cy.login(randUser, 'password')

cy.visit('/apps/files')

// wait a bit for things to be settled
cy.wait(1000)
})
after(function() {
cy.logout()
})

it('Open login background', function() {
const fileInfo = {
filename: '/core/img/logo/logo.png',
basename: 'logo.png',
mime: 'image/png',
source: '/core/img/logo/logo.png',
etag: 'abc',
hasPreview: false,
fileid: 123,
}

cy.window().then((win) => {
win.OCA.Viewer.open({
fileInfo,
list: [fileInfo],
})
})
})

it('Does not see a loading animation', function() {
cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
})

it('See the menu icon and title on the viewer header', function() {
cy.get('body > .viewer .modal-title').should('contain', 'logo.png')
cy.get('body > .viewer .modal-header button.action-item__menutoggle').should('be.visible')
cy.get('body > .viewer .modal-header button.header-close').should('be.visible')
})

it('Does not see navigation arrows', function() {
cy.get('body > .viewer a.prev').should('not.be.visible')
cy.get('body > .viewer a.next').should('not.be.visible')
})
})
39 changes: 37 additions & 2 deletions cypress/e2e/visual-regression.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('Visual regression tests ', function() {
cy.get('body > .viewer a.next').click()
cy.get('body > .viewer .modal-container img').should('have.length', 1)
cy.get('body > .viewer .modal-container img').should('have.attr', 'src')
cy.get('body > .viewer a.next').should('be.visible')
cy.get('body > .viewer a.prev').should('be.visible')
cy.get('body > .viewer a.next').should('be.visible')
})

Expand All @@ -112,7 +112,7 @@ describe('Visual regression tests ', function() {
cy.get('body > .viewer .modal-title').should('contain', 'test-card.png')
cy.get('body > .viewer .modal-container img').should('have.length', 1)
cy.get('body > .viewer .modal-container img').should('have.attr', 'src')
cy.get('body > .viewer a.next').should('be.visible')
cy.get('body > .viewer a.prev').should('be.visible')
cy.get('body > .viewer a.next').should('be.visible')
})

Expand All @@ -126,4 +126,39 @@ describe('Visual regression tests ', function() {
it('Take test-card.png screenshot 2', function() {
cy.compareSnapshot('image2')
})

it('Open non-dav image', function() {
const fileInfo = {
filename: '/core/img/logo/logo.png',
basename: 'logo.png',
mime: 'image/png',
source: '/core/img/logo/logo.png',
etag: 'abc',
hasPreview: false,
fileid: 123,
}

cy.window().then((win) => {
win.OCA.Viewer.open({
fileInfo,
list: [fileInfo],
})
})

cy.get('body > .viewer .modal-container img').should('have.length', 1)
cy.get('body > .viewer .modal-container img').should('have.attr', 'src')
cy.get('body > .viewer a.prev').should('not.be.visible')
cy.get('body > .viewer a.next').should('not.be.visible')
})

it('Does not see a loading animation', function() {
cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
})

it('Take non-dav logo.png screenshot', function() {
cy.compareSnapshot('non-dav')
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions js/viewer-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/viewer-main.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/components/ImageEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default {
return {
'data-theme-dark': true,
}
}
},
},
mounted() {
Expand Down
5 changes: 5 additions & 0 deletions src/mixins/Mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export default {
type: String,
required: true,
},
// file source to fetch contents from
source: {
type: String,
default: undefined,
},
// file path relative to user folder
hasPreview: {
type: Boolean,
Expand Down
8 changes: 7 additions & 1 deletion src/mixins/PreviewUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default {
previewpath() {
return this.getPreviewIfAny({
fileid: this.fileid,
source: this.source,
filename: this.filename,
hasPreview: this.hasPreview,
davPath: this.davPath,
Expand All @@ -60,12 +61,17 @@ export default {
*
* @param {object} data destructuring object
* @param {string} data.fileid the file id
* @param {?string} data.source the download source
* @param {boolean} data.hasPreview have the file an existing preview ?
* @param {string} data.davPath the absolute dav path
* @param {string} data.filename the file name
* @return {string} the absolute url
*/
getPreviewIfAny({ fileid, filename, hasPreview, davPath }) {
getPreviewIfAny({ fileid, source, filename, hasPreview, davPath }) {
if (source) {
return source
}

const searchParams = `fileId=${fileid}`
+ `&x=${Math.floor(screen.width * devicePixelRatio)}`
+ `&y=${Math.floor(screen.height * devicePixelRatio)}`
Expand Down
1 change: 1 addition & 0 deletions src/models/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function(fileInfo, mime, component) {
failed: false,
loaded: false,
davPath: getDavPath(fileInfo),
source: fileInfo.source ?? getDavPath(fileInfo),
}

return Object.assign({}, fileInfo, data)
Expand Down
45 changes: 39 additions & 6 deletions src/services/Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ import Images from '../models/images.js'
import Videos from '../models/videos.js'
import Audios from '../models/audios.js'

/**
* File info type definition
*
* @typedef {object} Fileinfo
* @property {string} filename the file name
* @property {string} basename the full path of the file
* @property {?string} source absolute path of a non-dav file, e.g. a static resource or provided by an app route
* @property {string} mime file MIME type in the format type/sub-type
* @property {string} etag WebDAV etag
* @property {boolean} hasPreview is there a WebDAV preview of this file?
* @property {number} fileid Nextcloud file ID
*/

export default class Viewer {

_state
Expand All @@ -33,6 +46,7 @@ export default class Viewer {
this._mimetypes = []
this._state = {}
this._state.file = ''
this._state.fileInfo = null
this._state.files = []
this._state.loadMore = () => ([])
this._state.onPrev = () => {}
Expand Down Expand Up @@ -81,11 +95,21 @@ export default class Viewer {
return this._state.file
}

/**
* Get the current opened file fileInfo
*
* @memberof Viewer
* @return {?Fileinfo} the currently opened file fileInfo
*/
get fileInfo() {
return this._state.fileInfo
}

/**
* Get the current files list
*
* @memberof Viewer
* @return {object[]} the currently opened file
* @return {Fileinfo[]} the current files list
*/
get files() {
return this._state.files
Expand Down Expand Up @@ -165,20 +189,24 @@ export default class Viewer {
*
* @memberof Viewer
* @param {object} options Options for opening the viewer
* @param {string} options.path path of the file to open
* @param {object[]} [options.list] the list of files as objects (fileinfo) format
* @param {?string} options.path path of the file to open
* @param {?Fileinfo} options.fileInfo file info of the file to open
* @param {Fileinfo[]} [options.list] the list of files as objects (fileinfo) format
* @param {Function} options.loadMore callback for loading more files
* @param {boolean} options.canLoop can the viewer loop over the array
* @param {Function} options.onPrev callback when navigating back to previous file
* @param {Function} options.onNext callback when navigation forward to next file
* @param {Function} options.onClose callback when closing the viewer
*/
open({ path, list = [], loadMore = () => ([]), canLoop = true, onPrev = () => {}, onNext = () => {}, onClose = () => {} } = {}) {
open({ path, fileInfo, list = [], loadMore = () => ([]), canLoop = true, onPrev = () => {}, onNext = () => {}, onClose = () => {} } = {}) {
if (typeof arguments[0] === 'string') {
throw new Error('Opening the viewer with a single string parameter is deprecated. Please use a destructuring object instead', `OCA.Viewer.open({ path: '${path}' })`)
}
if (!path && !fileInfo) {
throw new Error('Viewer needs either an URL or path to open. None given')
}

if (!path.startsWith('/')) {
if (path && !path.startsWith('/')) {
throw new Error('Please use an absolute path')
}

Expand All @@ -190,7 +218,12 @@ export default class Viewer {
throw new Error('The loadMore method must be a function')
}

this._state.file = path
// Only assign the one that is used to prevent false watcher runs
if (path) {
this._state.file = path
} else {
this._state.fileInfo = fileInfo
}
this._state.files = list
this._state.loadMore = loadMore
this._state.onPrev = onPrev
Expand Down
Loading

0 comments on commit a05dbf3

Please sign in to comment.