зеркало из https://github.com/nextcloud/viewer.git
Merge pull request #2467 from nextcloud/fix/refactor-for-31
fix: Adjust viewer for Nextcloud 31 public share UI
This commit is contained in:
Коммит
c625fa7513
|
@ -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,
|
||||
}))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче