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" /> -
-

{{ t('richdocuments', 'Custom Fonts') }}

- - - - {{ fontHint }} - - -
-					{{ 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') }} - -
+

{{ t('richdocuments', 'Secure View') }}

@@ -433,9 +412,8 @@ import SettingsCheckbox from './SettingsCheckbox.vue' import SettingsInputText from './SettingsInputText.vue' import SettingsSelectGroup from './SettingsSelectGroup.vue' import SettingsExternalApps from './SettingsExternalApps.vue' -import SettingsInputFile from './SettingsInputFile.vue' -import SettingsFontList from './SettingsFontList.vue' import GlobalTemplates from './AdminSettings/GlobalTemplates.vue' +import CustomFonts from './AdminSettings/CustomFonts.vue' import { getCurrentUser } from '@nextcloud/auth' import { isPublicShare, getSharingToken } from '@nextcloud/sharing/public' @@ -451,14 +429,6 @@ const SERVER_STATE_CONNECTION_ERROR = 2 const PROTOCOL_MISMATCH = 3 const SERVER_STATE_BROWSER_CONNECTION_ERROR = 4 -const fontMimes = [ - 'font/ttf', - 'font/otf', - 'application/font-sfnt', - 'font/opentype', - 'application/vnd.oasis.opendocument.formula-template', -] - export default { name: 'AdminSettings', components: { @@ -468,9 +438,8 @@ export default { NcSelect, NcSelectTags, SettingsExternalApps, - SettingsInputFile, - SettingsFontList, GlobalTemplates, + CustomFonts, NcModal, NcNoteCard, CoolFrame, @@ -498,10 +467,6 @@ export default { appUrl: generateUrl('/settings/apps/app-bundles/richdocumentscode'), approvedDemoModal: false, updating: false, - uploadingFont: false, - fontMimes, - fontHintUrl: window.location.protocol + '//' + window.location.host + generateUrl('/apps/richdocuments/settings/fonts.json'), - fontCustomDocumentUrl: 'https://docs.nextcloud.com/server/latest/admin_manual/office/configuration.html#custom-fonts', groups: [], tags: [], uiVisible: { @@ -529,7 +494,6 @@ export default { allTagsList: [], text: '', }, - fonts: [], hasSettingIframeSupport: false, setting_iframe_url: '', doc_format: null, @@ -554,24 +518,12 @@ export default { hasHostErrors() { return this.hostErrors.some(x => x) }, - fontHint() { - return t('richdocuments', 'Make sure to set this URL: {url} in the coolwsd.xml file of your Collabora Online server to ensure the added fonts get loaded automatically. Please note that http:// will only work for debug builds of Collabora Online. In production you must use https:// for remote font config.', - { url: this.fontHintUrl }, - ) - }, showFontConfigHint() { return this.serverMode !== 'builtin' }, shareToken() { return getSharingToken() }, - fontXmlHint() { - return ` - - ${this.fontHintUrl} - - ` - }, callbackUrl() { return this.settings.wopi_callback_url ? this.settings.wopi_callback_url : getCallbackBaseUrl() }, @@ -618,7 +570,6 @@ export default { } Vue.set(this.settings, 'edit_groups', this.settings.edit_groups ? this.settings.edit_groups.split('|') : null) Vue.set(this.settings, 'use_groups', this.settings.use_groups ? this.settings.use_groups.split('|') : null) - Vue.set(this.settings, 'fonts', this.initial.fonts ? this.initial.fonts : []) Vue.set(this.settings, 'hasSettingIframeSupport', this.initial.hasSettingIframeSupport ?? false) Vue.set(this.settings, 'setting_iframe_url', this.initial.setting_iframe_url ?? '') @@ -861,43 +812,6 @@ export default { return url.protocol }, - uploadFont(event) { - // TODO define font format list - const files = event.target.files - const file = files[0] - if (file.type !== '' && !fontMimes.includes(file.type)) { - showError(t('richdocuments', 'Font format not supported ({mime})', { mime: file.type })) - return - } - this.uploadingFont = true - - // Clear input to ensure that the change event will be emitted if - // the same file is picked again. - event.target.value = '' - - const formData = new FormData() - formData.append('fontfile', file) - const url = generateUrl('/apps/richdocuments/settings/fonts') - axios.post(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }).then((response) => { - // TODO reload font list - this.settings.fonts.push(file.name) - }).catch((error) => { - console.error(error) - showError(error?.response?.data?.error) - }).then(() => { - this.uploadingFont = false - }) - }, - onFontDeleted(name) { - const index = this.settings.fonts.indexOf(name) - if (index !== -1) { - this.settings.fonts.splice(index, 1) - } - }, }, } diff --git a/src/components/AdminSettings/CustomFonts.vue b/src/components/AdminSettings/CustomFonts.vue new file mode 100644 index 0000000000..56957b65e2 --- /dev/null +++ b/src/components/AdminSettings/CustomFonts.vue @@ -0,0 +1,237 @@ + + + + + + + diff --git a/src/components/AdminSettings/GlobalTemplates.vue b/src/components/AdminSettings/GlobalTemplates.vue index 5b1a82c7b3..8e0f3df888 100644 --- a/src/components/AdminSettings/GlobalTemplates.vue +++ b/src/components/AdminSettings/GlobalTemplates.vue @@ -11,6 +11,7 @@ type="file" class="hidden-visually" :accept="acceptedFileExtensions" + data-cy="newTemplateInput" @change="selectFile">
diff --git a/src/components/SettingsFont.vue b/src/components/SettingsFont.vue deleted file mode 100644 index 80a4ff9bb1..0000000000 --- a/src/components/SettingsFont.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - diff --git a/src/components/SettingsFontList.vue b/src/components/SettingsFontList.vue deleted file mode 100644 index 3452b6c5f2..0000000000 --- a/src/components/SettingsFontList.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - diff --git a/src/components/SettingsInputFile.vue b/src/components/SettingsInputFile.vue deleted file mode 100644 index cf54621823..0000000000 --- a/src/components/SettingsInputFile.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - diff --git a/src/stores/adminSettings.js b/src/stores/adminSettings.js new file mode 100644 index 0000000000..8bdb32f198 --- /dev/null +++ b/src/stores/adminSettings.js @@ -0,0 +1,12 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { defineStore } from 'pinia' + +export const useAdminSettingsStore = defineStore('adminSettings', { + state: () => ({ + fonts: [], + }), +})