Merge pull request #2467 from nextcloud/fix/refactor-for-31

fix: Adjust viewer for Nextcloud 31 public share UI
This commit is contained in:
Ferdinand Thiessen 2024-09-07 17:29:46 +02:00 коммит произвёл GitHub
Родитель 0389df4d2e eb1380d3c5
Коммит c625fa7513
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
37 изменённых файлов: 286 добавлений и 149667 удалений

Просмотреть файл

@ -1,69 +0,0 @@
/* roboto-cyrillic-ext-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(fonts/roboto-cyrillic-ext-400-normal.woff2) format('woff2'), url(fonts/roboto-cyrillic-ext-400-normal.woff) format('woff');
unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}
/* roboto-cyrillic-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(fonts/roboto-cyrillic-400-normal.woff2) format('woff2'), url(fonts/roboto-cyrillic-400-normal.woff) format('woff');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
/* roboto-greek-ext-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(data:font/woff2;base64,d09GMgABAAAAAAXYABIAAAAACgAAAAV8AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhQbHhw0BmAAXghUCYM8EQwKg2iDTgsUABIUATYCJAMkBCAFgnQHIAyCSRucCFFUkDIBfhzkZKcmnOCkolAQnoSyxYqFpYXieXb+xgO83X+ee8cPWukL624junMJs9RsoCpuxSopamF+Pi97f6xC6QrJFHRKpFCmII8L4C95A3y0rJouCIdkJgncD/oHj/4Ptd1Rm0IYXCtKb1yQDmO4797U2dlr/Om01LkOVpT2L12pLVa73QtgLIKxr2n9efiAIhiyAGBi25Coekds8qZninvglyCgAwhKQRDQspY1tF9oNA0iKTQN4IRtO0c8LdtzW2orwfLCWqaCzZfl11dfBsTdnaQ3h2kZ2eOZEwgbeF/yBVwQgZ2DvRwJfK0Dj4wLA3+w4XAB/M8FxYoIY9AzkO6S7TOGwEWk2ZbiCu/nOQToKUU5oq4x6zbE1HUGA3Rl83vzuil5fuJX+RchWrDatW53jdtwnlgZhpwlhXP0dtJr7vYxsPT/PLq40lhiT5ruQpdOGGT7LM3N6cMWw/ws9PIfIIcEwLJDUR3FYQgfiUJzxskPq2Qy1ggbiezOIJylETciLCiNZCMFSKaDJqeFHmOPg5ePsYC2syXS6aE6P7V6nJwhSOIdAc0Ke4n7Xb8SyWqkqXiaf5zcKrRPwvfqdPtrZBtL2slMnRuMa42LvcxYpRRZvA/n8T7tUCIaeZ2q3j7uEhVDkc8XZrrMEm9RfK85lv64HemnFa6lmfuYFI7x/oVR8InaSyj5acula+ve+LU96YKCxZUXd9MwGtXGUoutAUxK5q2NmLMD2mz+aZ2N4WzsRo9j+buXk1pEpRttzy1KfocMeUz6dmDs9k7cweWb9rsbsde9m5w+h/OOcb2wOG7o3RICJCrFpqEEFRhZH9oDuAjooYPPICPCo0jTpMlTRj1BOey1KZvbSstFFVnKclSBPKn7/nPJ6C8PU1DPT6+kYz8/gBNueLjm39PQ/QP9dT+ltmVK4aRWsRS+SabvokUfQ1Z/zGWygF8Mr9+/8b206dV6Ljp9GGVza+Jnt9+d8hVurXeJt93vjq6U3ZwJkOx4aa9k2z3+d04j7me6E29d13G+Vvxzc/2x9y4pOP96WSx98PKAi/qn3un2CdsyOa1xdLjn/jNOzIUF+AcAFPL/LuBa/t/+/00Wx7+LZarhcLSj7qhqn2s859Wt3etQ2/+kfRxqzc5ou8fJDwOT0QDzOKLLr2WqruDlpp0t2a9YhvLuvI6qnb1VNjpkZXJDl/FYKm5xTmMZ2tdaepL9fasvEPAi1srweZuqi+ubWBAA9duqOh3Aq2fXLZ48tfLYbwcI6FRFSox5GgsC4uTo+6gDX3L73r+JVpUAH39Qk4BvUOKXX+7fO5WxrANQiSBQXPZPRnXwL6t/kZURIvyq5E7nKYd/+oHsWlclBNZezqf/HAGhco/laHwB9IjiFIGA0gW4QlrhDPtsR9DxoiPqeXgx8S2mzZGZYXLk1qLzPbQCLlIvLSaeKN70nUj5TPIVKsUgsWqVoFQqU4hRrQ6jDiVahCAhYiUJsZwjklEiVgFWPdZyiRglGlTKVysFo1adMqxqRNRGB07ceCHMeI4bn835eBCWSYtaZUqUqmdxhRPCsFgl1zMIEaoVIhECVMoeIbGn6hD5JrKmH9WIUYTEBZpLoIi4tu4srS3CQRWXBjxE2jOODD23Tq8ZEC06EsK9yPGl5oa3Y1q4+6JJksQg5/nLSZoT4710FclcN06s6pO8JjvU0YoUM1dnec4lZWdJvIclqegQ1wVLSasxL8rVZtzuOy/2LOk8wKOF3qSrG3TEOel5b59dOyR9f+fF65a2B/EBlR2CR1LhYu2/fT32swx1OFfBLqCUehyHLE7hXvwPdkoD9sNc7GoobUO8bPge7JR6nItTeA3/g5/SgNk+RYQ6q0mgOgA=) format('woff2'), url(data:font/woff;base64,d09GRgABAAAAAATkAA4AAAAABXwAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHREVGAAABRAAAABQAAAAUAA8ACUdQT1MAAAFYAAAAHgAAAB5EdEx1R1NVQgAAAXgAAAAzAAAANJMNggJPUy8yAAABrAAAAEsAAABgdAng+GNtYXAAAAH4AAAAQAAAAF4+Y+J/Z2FzcAAAAjgAAAAMAAAADAAIABNnbHlmAAACRAAAARQAAAEUnMz0sGhlYWQAAANYAAAANgAAADb8atJ6aGhlYQAAA5AAAAAfAAAAJAq6BalobXR4AAADsAAAAB8AAAAkF+P/e2xvY2EAAAPQAAAAEwAAABQBMQGDbWF4cAAAA+QAAAAcAAAAIAArAOVuYW1lAAAEAAAAAM4AAAF0G504anBvc3QAAATQAAAAEwAAACD/bQBkAAEAAAAMAAAAAAAAAAEACAABAAEAAQAAAAoAHAAcAAFERkxUAAgABAAAAAD//wAAAAAAAHjaY2BkYGDgYjACQhYXN58QBrnkyqIcBqn0otRsBqmcxJI8BikGEGABEf//g0gA3Y0JHgB42iXFsRFAMAAAwE8ijCE76FQ6lbNFRlGr01jBKhZyCt+81B3hlMktT4TxPz1qRASwkABg3ffVwrj1XurQoiJc0M35VhTpA+O9Ck4AeNpjYGBgAmJmIBYBkoxgmoXBC0jzMXAA5djAKngZFBgWyPv+/w/kofBBOv5/+//kf/qD3WDdPAwIwAQA0KYN+QABAAIACAAC//8ADwAFAGQAAAMoBbAAAwAGAAkADAAPAAAhIREhAxEBAREBAyEBNQEhAyj9PALENv7u/roBDOQCA/7+AQL9/QWw+qQFB/19Anf7EQJ4/V4CXogCXgAAAgB2/+wFCQXEABEAHwAAARQCBCMiJAInNTQSJDMyBBIVJxACIyICBxUUEjMyEjcFCZD++LCs/vaTApIBC6yvAQuQv9C7ttED07m6zAMCqdb+waipATnOadIBQqup/r/VAgEDARX+6/Zr+/7hAQ/9AAIAbwRwAskF1gAFAA0AAAETMxUDIwEzFRYXByY1AZF0xN9Z/t6oA1BJsgSUAUIV/sMBUlt7VTtfu////jL/7AVPBdYAJgAFRgAABwAG/cMAAAABAAAAAiMSo8X+nl8PPPUAGQgAAAAAAMTwES4AAAAA1QFS9Pob/dUJMAhzAAAACQACAAAAAAAAeNpjYGRgYM/5x8PAwOn5S/qfF6cBUAQVcAIAb4cEcQB42mPuYUhhgALG3xDM2sBQxqzAkA9mH/tnBABopAdwAHjaY2Bg0ITDRIY6IOwCABGeArUAeNpjYGRgYOBk6GcQYwhhYAXzEICNgREAGIoBEXjaXY4BBgJRFEVPVSnSCkIgoKkKUSBJIqESIKp+05BpzFRpI62gBbTErvGNkes+977nfB8ocSJHJl8GtnxtzlDhY3OWKm+bc6l9PpULNAhsLlJjbXNVCc7cpIABLekZy2FHIB90NWpXQlxdL3jaGXwizUibOTPGTFiw0mzSxaHNUsRevslNNSP6LnpHyEYtFOvp5lOPiQ49+gzj1lbr/zHp98ZywEtbDxf9PqE6SlOukivOqM3wOeAojbhIdZYJFcXNEMkhD80jzg9HQTQoAAB42mNgZgCD/1kMKQxYAAAqHwHRAA==) format('woff');
unicode-range: U+1F00-1FFF;
}
/* roboto-greek-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(fonts/roboto-greek-400-normal.woff2) format('woff2'), url(fonts/roboto-greek-400-normal.woff) format('woff');
unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}
/* roboto-vietnamese-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(fonts/roboto-vietnamese-400-normal.woff2) format('woff2'), url(fonts/roboto-vietnamese-400-normal.woff) format('woff');
unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}
/* roboto-latin-ext-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(fonts/roboto-latin-ext-400-normal.woff2) format('woff2'), url(fonts/roboto-latin-ext-400-normal.woff) format('woff');
unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
/* roboto-latin-400-normal */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(fonts/roboto-latin-400-normal.woff2) format('woff2'), url(fonts/roboto-latin-400-normal.woff) format('woff');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1,2 +1,2 @@
/* extracted by css-entry-points-plugin */
@import './main-BT4PqNwX.chunk.css';
@import './main-DXSti9TM.chunk.css';

Просмотреть файл

@ -152,7 +152,7 @@ export default function(file, type, sidebar = false) {
})
it('Open the viewer on file click (public)', function() {
cy.openFileInShare(placedName)
cy.openFile(placedName)
cy.get('body > .viewer').should('be.visible')
})

Просмотреть файл

@ -36,10 +36,6 @@ describe(`Download ${fileName} in viewer`, function() {
cy.visit('/apps/files')
})
})
after(function() {
// already logged out after visiting share link
// cy.logout()
})
it('See the default files list', function() {
cy.getFile('welcome.txt').should('contain', 'welcome .txt')
@ -62,7 +58,7 @@ describe(`Download ${fileName} in viewer`, function() {
}
})
cy.createLinkShare('/Photos').then(token => {
cy.createLinkShare('/Photos').then((token: string) => {
cy.intercept('GET', '**/apps/files_sharing/api/v1/shares*').as('sharingAPI')
// Open the sidebar from the breadcrumbs
@ -80,25 +76,50 @@ describe(`Download ${fileName} in viewer`, function() {
cy.get('@hideDownloadBtn').get('span').contains('Hide download').click()
cy.get('@hideDownloadBtn').get('input[type=checkbox]').should('be.checked')
cy.intercept('PUT', '/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
cy.contains('button', 'Update share').click()
cy.wait('@updateShare')
// Log out and access link share
cy.logout()
cy.visit(`/s/${token}`)
})
})
it('See only view action', () => {
for (const file of ['image1.jpg', 'image2.jpg']) {
cy.get(`[data-cy-files-list-row-name="${CSS.escape(file)}"]`)
.find('[data-cy-files-list-row-actions]')
.find('button')
.click()
// Only view action
cy.get('[role="menu"]:visible')
.find('button')
.should('have.length', 1)
.first()
.should('contain.text', 'View')
cy.get(`[data-cy-files-list-row-name="${CSS.escape(file)}"]`)
.find('[data-cy-files-list-row-actions]')
.find('button')
.click()
}
})
it('Open the viewer on file click', function() {
cy.openFileInShare('image1.jpg')
cy.openFile('image1.jpg')
cy.get('body > .viewer').should('be.visible')
})
it('Does not see a loading animation', function() {
// TODO: FIX DOWNLOAD DISABLED SHARES
it.skip('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 title on the viewer header but not the Download nor the menu button', function() {
// TODO: FIX DOWNLOAD DISABLED SHARES
it.skip('See the title on the viewer header but not the Download nor the menu button', function() {
cy.get('body > .viewer .modal-header__name').should('contain', 'image1.jpg')
cy.get('body a[download="image1.jpg"]').should('not.exist')
cy.get('body > .viewer .modal-header button.action-item__menutoggle').should('not.exist')

Просмотреть файл

@ -67,7 +67,7 @@ describe(`Download ${fileName} from viewer in link share`, function() {
})
it('Open the viewer on file click', function() {
cy.openFileInShare('image1.jpg')
cy.openFile('image1.jpg')
cy.get('body > .viewer').should('be.visible')
})

Просмотреть файл

@ -69,7 +69,7 @@ describe('See shared folder with link share', function() {
})
it('Open the viewer on file click', function() {
cy.openFileInShare('image1.jpg')
cy.openFile('image1.jpg')
cy.get('body > .viewer').should('be.visible')
})

Просмотреть файл

@ -47,20 +47,52 @@ describe('See shared folder with link share', function() {
it('Opens the shared image in the viewer', function() {
cy.visit(`/s/${imageToken}`)
cy.get('#imgframe img').should('be.visible')
cy.get('#imgframe > #viewer').should('be.visible')
cy.contains('image1.jpg').should('be.visible')
cy.scrollTo('bottom', { ensureScrollable: false })
cy.get(`#header a[href*="/s/${imageToken}/download"]`).should('be.visible')
cy.intercept('GET', '**/apps/files_sharing/publicpreview/**').as('getImage')
cy.openFileInSingleShare()
cy.wait('@getImage')
.its('response.statusCode')
.should('eq', 200)
// Make sure loading is finished
cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
// The image source is the preview url
cy.get('body > .viewer .modal-container .viewer__file.viewer__file--active img')
.should('have.attr', 'src')
.and('contain', '/apps/files_sharing/publicpreview/')
// See the menu icon and close button
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('Opens the shared video in the viewer', function() {
cy.visit(`/s/${videoToken}`)
cy.get('#imgframe .plyr').should('be.visible')
cy.get('#imgframe > #viewer').should('be.visible')
cy.contains('video1.mp4').should('be.visible')
cy.scrollTo('bottom', { ensureScrollable: false })
cy.get(`#header a[href*="/s/${videoToken}/download"]`).should('be.visible')
cy.intercept('GET', '**/public.php/dav/files/**').as('loadVideo')
cy.openFileInSingleShare()
cy.wait('@loadVideo')
// Make sure loading is finished
cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
// The video source is the preview url
cy.get('body > .viewer .modal-container .viewer__file.viewer__file--active video')
.should('have.attr', 'src')
.and('contain', `/public.php/dav/files/${videoToken}`)
// See the menu icon and close button
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')
})
})

Просмотреть файл

@ -28,7 +28,7 @@ import { addCompareSnapshotCommand } from 'cypress-visual-regression/dist/comman
addCommands()
addCompareSnapshotCommand()
const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '')
const url = Cypress.config('baseUrl')!.replace(/\/index.php\/?$/g, '')
Cypress.env('baseUrl', url)
/**
@ -97,8 +97,10 @@ Cypress.Commands.add('openFile', fileName => {
cy.wait(250)
})
Cypress.Commands.add('openFileInShare', fileName => {
cy.get(`.files-fileList tr[data-file="${CSS.escape(fileName)}"] a.name`).click()
Cypress.Commands.add('openFileInSingleShare', () => {
cy.get('tr[data-cy-files-list-row-name]')
.should('have.length', 1)
.click()
// eslint-disable-next-line
cy.wait(250)
})

Просмотреть файл

@ -1,4 +1,12 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts"],
"include": ["../*.ts", "."],
"compilerOptions": {
"rootDir": "..",
"types": [
"cypress",
"dockerode",
"node"
]
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,73 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node, View } from '@nextcloud/files'
import { DefaultType, FileAction, Permission, registerFileAction } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import svgEye from '@mdi/svg/svg/eye.svg?raw'
/**
* @param node The file to open
* @param view any The files view
* @param dir the directory path
*/
function pushToHistory(node: Node, view: View, dir: string) {
window.OCP.Files.Router.goToRoute(
null,
{ view: view.id, fileid: String(node.fileid) },
{ dir, openfile: 'true' },
true,
)
}
/**
* Execute the viewer files action
* @param node The active node
* @param view The current view
* @param dir The current path
*/
async function execAction(node: Node, view: View, dir: string): Promise<boolean|null> {
const oldRoute = [
window.OCP.Files.Router.name,
{ ...window.OCP.Files.Router.params },
{ ...window.OCP.Files.Router.query } as Record<string, string>,
true,
] as const
const onClose = () => {
// This can sometime be called with the openfile set to true already. But we don't want to keep openfile when closing the viewer.
delete oldRoute[2].openfile
window.OCP.Files.Router.goToRoute(...oldRoute)
}
pushToHistory(node, view, dir)
window.OCA.Viewer.open({ path: node.path, onPrev: pushToHistory, onNext: pushToHistory, onClose })
return null
}
/**
* Register the viewer action on the files API
*/
export function registerViewerAction() {
registerFileAction(new FileAction({
id: 'view',
displayName: () => t('viewer', 'View'),
iconSvgInline: () => svgEye,
default: DefaultType.DEFAULT,
enabled: (nodes) => {
// Disable if not located in user root
if (nodes.some(node => !(node.isDavRessource && node.root?.startsWith('/files')))) {
return false
}
return nodes.every((node) =>
Boolean(node.permissions & Permission.READ)
&& window.OCA.Viewer.mimetypes.includes(node.mime),
)
},
exec: execAction,
}))
}

18
src/global.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/// <reference types="@nextcloud/typings" />
import type Viewer from './services/Viewer.js'
declare global {
interface Window {
OCA: {
Viewer: Viewer
}
OCP: Nextcloud.v29.OCP
}
}
export {}

Просмотреть файл

@ -1,58 +0,0 @@
/**
* @copyright Copyright (c) 2020 Azul <azul@riseup.net>
*
* @author Azul <azul@riseup.net>
*
* @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/>.
*
*/
/**
* @param {Node} node The file to open
* @param {any} view any The files view
* @param {string} dir the directory path
*/
export default function(node, view, dir) {
// replace potential leading double slashes
const path = `${node.dirname}/${node.basename}`.replace(/^\/\//, '/')
const oldRoute = [
window.OCP.Files.Router.name,
{ ...window.OCP.Files.Router.params },
{ ...window.OCP.Files.Router.query },
true,
]
const onClose = () => {
// This can sometime be called with the openfile set to true already. But we don't want to keep openfile when closing the viewer.
delete oldRoute[2].openfile
window.OCP.Files.Router.goToRoute(...oldRoute)
}
pushToHistory(node, view, dir)
OCA.Viewer.open({ path, onPrev: pushToHistory, onNext: pushToHistory, onClose })
}
/**
* @param {Node} node The file to open
* @param {any} view any The files view
* @param {string} dir the directory path
*/
function pushToHistory(node, view, dir) {
window.OCP.Files.Router.goToRoute(
null,
{ view: view.id, fileid: node.fileid },
{ dir, openfile: true },
true,
)
}

Просмотреть файл

@ -1,55 +0,0 @@
/**
* @copyright Copyright (c) 2020 Azul <azul@riseup.net>
*
* @author Azul <azul@riseup.net>
*
* @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 { encodePath } from '@nextcloud/paths'
/**
* @param {string} name the file name
* @param {object} context the file context
*/
export default function(name, context) {
// replace potential leading double slashes
const path = `${context.dir}/${name}`.replace(/^\/\//, '/')
const oldQuery = location.search.replace(/^\?/, '')
const onClose = () => OC.Util.History.pushState(oldQuery)
if (!context.fileInfoModel && context.fileList) {
context.fileInfoModel = context.fileList.getModelForFile(name)
}
if (context.fileInfoModel) {
pushToHistory({ fileid: context.fileInfoModel.get('id') })
}
OCA.Viewer.open({ path, onPrev: pushToHistory, onNext: pushToHistory, onClose })
}
/**
* @param {object} root destructuring object
* @param {number} root.fileid the opened file ID
*/
function pushToHistory({ fileid }) {
const params = OC.Util.History.parseUrlQuery()
const dir = params.dir
delete params.dir
delete params.fileid
params.openfile = fileid
const query = 'dir=' + encodePath(dir) + '&' + OC.buildQueryString(params)
OC.Util.History.pushState(query)
}

9
src/shims.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
declare module '*.svg?raw' {
const content: string
export default content
}

Просмотреть файл

@ -20,21 +20,42 @@
*
*/
import type { FileStat } from 'webdav'
import { davRemoteURL, davRootPath } from '@nextcloud/files'
import { getLanguage } from '@nextcloud/l10n'
import { encodePath } from '@nextcloud/paths'
import camelcase from 'camelcase'
import { isNumber } from './numberUtil'
import { davRemoteURL, davRootPath } from '@nextcloud/files'
declare const OC: Nextcloud.v29.OC
export interface FileInfo {
/** Filename (name with path) */
filename: string
/** Basename of the file */
basename: string
/** DAV source URL */
source: string
/** File size in bytes */
size: number
/** E-Tag */
etag?: string
/** MIME type */
mime?: string
/** Last modification date */
lastmod?: string
/** File is marked as favorite */
isFavorite?: boolean
/** File type */
type: 'directory'|'file'
}
/**
* Extract dir and name from file path
*
* @param {string} path the full path
* @return {string[]} [dirPath, fileName]
* @param path the full path
* @return [dirPath, fileName]
*/
const extractFilePaths = function(path) {
export function extractFilePaths(path: string): [string, string] {
const pathSections = path.split('/')
const fileName = pathSections[pathSections.length - 1]
const dirPath = pathSections.slice(0, pathSections.length - 1).join('/')
@ -44,13 +65,12 @@ const extractFilePaths = function(path) {
/**
* Sorting comparison function
*
* @param {object} fileInfo1 file 1 fileinfo
* @param {object} fileInfo2 file 2 fileinfo
* @param {string} key key to sort with
* @param {boolean} [asc] sort ascending?
* @return {number}
* @param fileInfo1 file 1 FileInfo
* @param fileInfo2 file 2 FileInfo
* @param key key to sort with
* @param asc sort ascending (default true)
*/
const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) {
export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: string, asc = true): number {
if (fileInfo1.isFavorite && !fileInfo2.isFavorite) {
return -1
@ -72,23 +92,21 @@ const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) {
}
// sort by date if key is lastmod
if (key === 'lastmod') {
const result = new Date(fileInfo1[key]).getTime() - new Date(fileInfo2[key]).getTime()
const result = new Date(fileInfo1.lastmod ?? 0).getTime() - new Date(fileInfo2.lastmod ?? 0).getTime()
return asc ? -result : result
}
// finally sort by name
return asc
? fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage(), { numeric: true })
: -fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage(), { numeric: true })
? fileInfo1[key].localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
: -fileInfo1[key].localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
}
export type FileInfo = object
/**
* Generate a fileinfo object based on the full dav properties
* Generate a FileInfo object based on the full dav properties
* It will flatten everything and put all keys to camelCase
* @param obj
* @param obj The stat response to convert
*/
const genFileInfo = function(obj: FileStat): FileInfo {
export function genFileInfo(obj: FileStat): FileInfo {
const fileInfo = {}
Object.keys(obj).forEach(key => {
@ -110,7 +128,8 @@ const genFileInfo = function(obj: FileStat): FileInfo {
}
}
})
return fileInfo
return fileInfo as FileInfo
}
/**
@ -120,25 +139,19 @@ const genFileInfo = function(obj: FileStat): FileInfo {
* @param fileInfo.filename the file full path
* @param fileInfo.source the file source if any
*/
function getDavPath({ filename, source = '' }: { filename: string, source?: string }): string|null {
// TODO: allow proper dav access without the need of basic auth
// https://github.com/nextcloud/server/issues/19700
const prefixUser = davRootPath
export function getDavPath({ filename, source = '' }: { filename: string, source?: string }): string|null {
if (!filename || typeof filename !== 'string') {
return null
}
// If we have a source but we're not a dav resource, return null
if (source && !source.includes(prefixUser)) {
if (source && !source.includes(davRootPath)) {
return null
}
// Workaround for files with different root like /remote.php/dav
if (!filename.startsWith(prefixUser)) {
if (!filename.startsWith(davRootPath)) {
filename = `${davRootPath}${filename}`
}
return davRemoteURL + encodePath(filename)
}
export { extractFilePaths, sortCompare, genFileInfo, getDavPath }

Просмотреть файл

@ -195,20 +195,19 @@ import Vue from 'vue'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { registerFileAction, FileAction, Permission, DefaultType, Node, davRemoteURL, davRootPath } from '@nextcloud/files'
import { Node, davRemoteURL, davRootPath } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import getSortingConfig from '../services/FileSortingConfig.ts'
import isFullscreen from '@nextcloud/vue/dist/Mixins/isFullscreen.js'
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js'
import { extractFilePaths, sortCompare } from '../utils/fileUtils.ts'
import { registerViewerAction } from '../files_actions/viewerAction.ts'
import getSortingConfig from '../services/FileSortingConfig.ts'
import canDownload from '../utils/canDownload.js'
import cancelableRequest from '../utils/CancelableRequest.js'
import Error from '../components/Error.vue'
import File from '../models/file.js'
import filesActionHandler from '../services/FilesActionHandler.js'
import legacyFilesActionHandler from '../services/LegacyFilesActionHandler.js'
import getFileInfo from '../services/FileInfo.ts'
import getFileList from '../services/FileList.ts'
import Mime from '../mixins/Mime.js'
@ -216,7 +215,6 @@ import logger from '../services/logger.js'
import Delete from 'vue-material-design-icons/Delete.vue'
import Download from 'vue-material-design-icons/Download.vue'
import EyeSvg from '@mdi/svg/svg/eye.svg?raw'
import Fullscreen from 'vue-material-design-icons/Fullscreen.vue'
import FullscreenExit from 'vue-material-design-icons/FullscreenExit.vue'
import Pencil from 'vue-material-design-icons/Pencil.vue'
@ -727,7 +725,7 @@ export default {
}
// get saved fileInfo
fileInfo = this.fileList[this.currentIndex]
fileInfo = this.fileList[this.currentIndex] ?? fileInfo
// show file
this.currentFile = new File(fileInfo, mime, handler.component)
@ -798,12 +796,12 @@ export default {
},
/**
* Registering possible new handers
* Registering possible new handlers
*
* @param {object} handler the handler to register
* @param {string} handler.id unique handler identifier
* @param {Array} handler.mimes list of valid mimes compatible with the handler
* @param {object} handler.component a vuejs component to render when a file matching the mime list is opened
* @param {object} handler.component a VueJs component to render when a file matching the mime list is opened
* @param {string} [handler.group] a group name to be associated with for the slideshow
*/
registerHandler(handler) {
@ -848,8 +846,6 @@ export default {
return
}
// register file action and groups
this.registerLegacyAction({ mime, group: handler.group })
// register groups
this.registerGroups({ mime, group: handler.group })
@ -887,8 +883,6 @@ export default {
return
}
// register file action and groups if the request alias had a group
this.registerLegacyAction({ mime, group: this.mimeGroups[alias] })
// register groups if the request alias had a group
this.registerGroups({ mime, group: this.mimeGroups[alias] })
@ -901,31 +895,6 @@ export default {
}
},
registerLegacyAction({ mime, group }) {
if (!this.isStandalone && OCA?.Files?.fileActions) {
// unregistered handler, let's go!
OCA.Files.fileActions.registerAction({
name: 'view',
displayName: t('viewer', 'View'),
mime,
permissions: OC.PERMISSION_READ,
actionHandler: legacyFilesActionHandler,
})
OCA.Files.fileActions.setDefault(mime, 'view')
logger.debug('Legacy file action registered for mime ' + mime, { mime, group })
}
// register groups
if (group) {
this.mimeGroups[mime] = group
// init if undefined
if (!this.mimeGroups[group]) {
this.mimeGroups[group] = []
}
this.mimeGroups[group].push(mime)
}
},
registerGroups({ mime, group }) {
if (group) {
this.mimeGroups[mime] = group
@ -939,26 +908,7 @@ export default {
registerFileActions() {
if (!this.isStandalone) {
registerFileAction(new FileAction({
id: 'view',
displayName() {
return t('viewer', 'View')
},
iconSvgInline: () => EyeSvg,
default: DefaultType.DEFAULT,
enabled: (nodes) => {
// Disable if not located in user root
if (nodes.some(node => !(node.isDavRessource && node.root?.startsWith('/files')))) {
return false
}
// Faster to check if at least one node doesn't match the requirements
return !nodes.some(node => (
(node.permissions & Permission.READ) === 0
|| !this.Viewer.mimetypes.includes(node.mime)
))
},
exec: filesActionHandler,
}))
registerViewerAction()
}
},

Просмотреть файл

@ -1,17 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./src/**.ts", "*.ts"],
"include": ["./src"],
"compilerOptions": {
"types": ["cypress", "cypress-visual-regression", "node", "dockerode", "@nextcloud/typings"],
"target": "ESNext",
"module": "esnext",
"moduleResolution": "Bundler",
// Allow ts to import js files
"allowJs": true,
"allowSyntheticDefaultImports": true,
"declaration": false,
"lib": [
"DOM",
"ES2015"
],
"rootDir": "src",
"noImplicitAny": false,
"resolveJsonModule": true,
"strict": true,
},
"vueCompilerOptions": {
"target": 2.7
}
}