diff --git a/cypress/e2e/settings.spec.js b/cypress/e2e/settings.spec.js index 5aff132b20..2b7301e804 100644 --- a/cypress/e2e/settings.spec.js +++ b/cypress/e2e/settings.spec.js @@ -6,7 +6,6 @@ import { User } from '@nextcloud/e2e-test-server/cypress' const usesHttps = Cypress.config('baseUrl').startsWith('https') const collaboraUrl = Cypress.env('collaboraUrl') -const defaultFonts = ['AmaticSC-Regular.ttf'] describe('Office admin settings', function() { @@ -65,16 +64,51 @@ describe('Office admin settings', function() { .scrollIntoView() .should('be.visible') - cy.get('#font-settings') + cy.get('.settings-section__name') + .contains('Global Templates') .scrollIntoView() .should('be.visible') - defaultFonts.forEach(font => { - cy.get('.settings-entry.font-list-settings').contains(font) - }) + }) +}) + +describe('Custom Fonts', function() { + const defaultFonts = ['AmaticSC-Regular.ttf'] + + beforeEach(function() { + cy.login(new User('admin', 'admin')) + cy.visit('/settings/admin/richdocuments') cy.get('.settings-section__name') - .contains('Global Templates') + .contains('Custom Fonts') .scrollIntoView() .should('be.visible') }) + + it('Can delete a font', function() { + cy.intercept({ + method: 'DELETE', + url: `**/richdocuments/settings/fonts/${defaultFonts[0]}`, + }).as('deleteFontRequest') + + cy.get(`button[aria-label="Delete ${defaultFonts[0]}"]`) + .click({ force: true }) + + cy.wait('@deleteFontRequest').its('response.statusCode').should('eq', 200) + }) + + it('Can upload a font file', function() { + cy.intercept({ + method: 'POST', + url: '**/richdocuments/settings/fonts', + }).as('uploadFontRequest') + + cy.uploadInputFile({ + identifier: 'newFontInput', + fixturePath: `fonts/${defaultFonts[0]}`, + fileName: defaultFonts[0], + mimeType: 'font/ttf', + }) + + cy.wait('@uploadFontRequest').its('response.statusCode').should('eq', 200) + }) }) diff --git a/cypress/e2e/templates.spec.js b/cypress/e2e/templates.spec.js index d375250094..da0d8b9c13 100644 --- a/cypress/e2e/templates.spec.js +++ b/cypress/e2e/templates.spec.js @@ -19,8 +19,12 @@ describe('Global templates', function() { }) it('Can be uploaded', function() { + cy.login(new User('admin', 'admin')) + cy.visit('/settings/admin/richdocuments') + cy.intercept('POST', '**/richdocuments/template').as('templateUploadRequest') - cy.uploadSystemTemplate({ + cy.uploadInputFile({ + identifier: 'newTemplateInput', fixturePath: 'templates/presentation.otp', fileName: 'systemtemplate.otp', mimeType: 'application/vnd.oasis.opendocument.presentation-template', @@ -34,7 +38,11 @@ describe('Global templates', function() { }) it('Can prevent uploading a duplicate', function() { - cy.uploadSystemTemplate({ + cy.login(new User('admin', 'admin')) + cy.visit('/settings/admin/richdocuments') + + cy.uploadInputFile({ + identifier: 'newTemplateInput', fixturePath: 'templates/presentation.otp', fileName: 'systemtemplate.otp', mimeType: 'application/vnd.oasis.opendocument.presentation-template', diff --git a/cypress/fixtures/fonts/AmaticSC-Regular.ttf b/cypress/fixtures/fonts/AmaticSC-Regular.ttf new file mode 100644 index 0000000000..fc4c36261a Binary files /dev/null and b/cypress/fixtures/fonts/AmaticSC-Regular.ttf differ diff --git a/cypress/fixtures/fonts/AmaticSC-Regular.ttf.license b/cypress/fixtures/fonts/AmaticSC-Regular.ttf.license new file mode 100644 index 0000000000..2061c9529c --- /dev/null +++ b/cypress/fixtures/fonts/AmaticSC-Regular.ttf.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2015 The Amatic SC Project Authors (https://github.com/googlefonts/AmaticSC) +SPDX-License-Identifier: OFL-1.1 diff --git a/cypress/support/commands.js b/cypress/support/commands.js index b088575281..b092b70e50 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -376,11 +376,28 @@ Cypress.Commands.add('uploadSystemTemplate', ({ fixturePath, fileName, mimeType cy.login(new User('admin', 'admin')) cy.visit('/settings/admin/richdocuments') - cy.get('.settings-section__name') - .contains('Global Templates') + cy.get('.settings-section') + .find('input[data-cy="newTemplateInput"]') + .as('newTemplateInput') + + cy.get('@newTemplateInput') .scrollIntoView() - cy.get('.settings-section input[type="file"]').selectFile({ + cy.get('@newTemplateInput').selectFile({ + contents: `cypress/fixtures/${fixturePath}`, + fileName, + mimeType, + }, { force: true }) +}) + +Cypress.Commands.add('uploadInputFile', ({ identifier, fixturePath, fileName, mimeType }) => { + cy.get('.settings-section') + .find(`input[data-cy="${identifier}"]`) + .as('fileInputElement') + + cy.get('@fileInputElement').scrollIntoView() + + cy.get('@fileInputElement').selectFile({ contents: `cypress/fixtures/${fixturePath}`, fileName, mimeType, diff --git a/lib/Service/FontService.php b/lib/Service/FontService.php index ce21fe1d30..38f3ae2a9e 100644 --- a/lib/Service/FontService.php +++ b/lib/Service/FontService.php @@ -188,9 +188,13 @@ private function generateFontOverview(ISimpleFile $fontFile): void { fflush($tmpFontFile); $im = imagecreatetruecolor(250, 30); - $bg_color = imagecolorallocate($im, 255, 255, 255); + imagealphablending($im, false); + + $bg_color = imagecolorallocatealpha($im, 255, 255, 255, 127); $font_color = imagecolorallocate($im, $color[0], $color[1], $color[2]); - imagefilledrectangle($im, 0, 0, 399, 29, $bg_color); + imagefill($im, 0, 0, $bg_color); + imagesavealpha($im, true); + $ttfResult = imagettftext($im, 20, 0, 0, 22, $font_color, $tmpFontFilePath, $text); // this happens with invalid ttf fonts if ($ttfResult === false) { diff --git a/package-lock.json b/package-lock.json index cf6dbed876..78ae46806d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@nextcloud/viewer": "^1.0.0", "@nextcloud/vue": "^8.33.0", "jquery": "^4.0.0", + "pinia": "^2.3.1", "vue": "^2.7.16", "vue-material-design-icons": "^5.3.1" }, @@ -5964,6 +5965,12 @@ "dev": true, "peer": true }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@vue/eslint-config-typescript": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", @@ -16673,6 +16680,54 @@ "node": ">=0.10.0" } }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -21025,7 +21080,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26557,6 +26612,11 @@ } } }, + "@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, "@vue/eslint-config-typescript": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", @@ -33934,6 +33994,23 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, + "pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "requires": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "requires": {} + } + } + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -36929,7 +37006,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "devOptional": true }, "typescript-event-target": { "version": "1.1.2", diff --git a/package.json b/package.json index 375172351b..3f03a563f8 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@nextcloud/viewer": "^1.0.0", "@nextcloud/vue": "^8.33.0", "jquery": "^4.0.0", + "pinia": "^2.3.1", "vue": "^2.7.16", "vue-material-design-icons": "^5.3.1" }, diff --git a/src/admin.js b/src/admin.js index 1bea381dba..3477026c16 100644 --- a/src/admin.js +++ b/src/admin.js @@ -5,6 +5,7 @@ import './init-shared.js' import Vue from 'vue' import AdminSettings from './components/AdminSettings.vue' +import { createPinia, PiniaVuePlugin } from 'pinia' import '../css/admin.scss' // CSP config for webpack dynamic chunk loading @@ -21,9 +22,26 @@ Vue.prototype.n = n Vue.prototype.OC = OC Vue.prototype.OCA = OCA +Vue.use(PiniaVuePlugin) + +const pinia = createPinia() const element = document.getElementById('admin-vue') +const initialSettings = JSON.parse(element.dataset.initial) + +// Hydrate the adminSettings store with the initial settings +// provided by the initial state +pinia.state.value = { + adminSettings: { + fonts: initialSettings.fonts, + }, +} /* eslint-disable-next-line no-new */ new Vue({ - render: h => h(AdminSettings, { props: { initial: JSON.parse(element.dataset.initial) } }), + pinia, + render: h => h( + AdminSettings, + { + props: { initial: JSON.parse(element.dataset.initial) }, + }), }).$mount('#admin-vue') diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 1d610eb5af..ffdff01114 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -285,28 +285,7 @@ @update="updateWopiAllowlist" /> -
- {{ fontXmlHint }}
-
-
-
- {{ t('richdocuments', 'For ideal document compatibility we recommend you to install commonly used fonts. If your users are working with Microsoft Office, installing their proprietary fonts can be done following the documentation.') }} {{ t('richdocuments', 'Custom fonts documentation') }}
-
- | + {{ font }} + | +
+ |
+
+ |
+
+ {{ fontXmlHint }}
+
+
+
+ {{ t('richdocuments', 'For ideal document compatibility we recommend you to install commonly used fonts. If your users are working with Microsoft Office, installing their proprietary fonts can be done following the documentation.') }} {{ t('richdocuments', 'Custom fonts documentation') }}
+
+