@silexlabs/silex 3.0.1-alpha.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.default +20 -0
- package/.silex.js +67 -8
- package/LICENSE +661 -203
- package/README.md +37 -35
- package/dist/client/client/api.d.ts +90 -0
- package/dist/client/client/assetUrl.d.ts +54 -0
- package/dist/client/client/config.d.ts +102 -0
- package/dist/client/client/events.d.ts +23 -0
- package/dist/client/client/expose.d.ts +8 -0
- package/dist/client/client/grapesjs/LoginDialog.d.ts +15 -0
- package/dist/client/client/grapesjs/PublicationManager.d.ts +73 -0
- package/dist/client/client/grapesjs/PublicationUi.d.ts +59 -0
- package/dist/client/client/grapesjs/blocks.d.ts +1 -0
- package/dist/client/client/grapesjs/breadcrumbs.d.ts +1 -0
- package/dist/client/client/grapesjs/css-props.d.ts +6 -0
- package/dist/client/client/grapesjs/footer.d.ts +2 -0
- package/dist/client/client/grapesjs/img.d.ts +6 -0
- package/dist/client/client/grapesjs/index.d.ts +9 -0
- package/dist/client/client/grapesjs/internal-links.d.ts +1 -0
- package/dist/client/client/grapesjs/keymaps.d.ts +123 -0
- package/dist/client/client/grapesjs/new-page-dialog.d.ts +2 -0
- package/dist/client/client/grapesjs/openImport.d.ts +9 -0
- package/dist/client/client/grapesjs/page-panel.d.ts +9 -0
- package/dist/client/client/grapesjs/project-bar.d.ts +21 -0
- package/dist/client/client/grapesjs/resize-panel.d.ts +1 -0
- package/dist/client/client/grapesjs/rich-text.d.ts +4 -0
- package/dist/client/client/grapesjs/semantic.d.ts +1 -0
- package/dist/client/client/grapesjs/settings-sections.d.ts +16 -0
- package/dist/client/client/grapesjs/settings.d.ts +4 -0
- package/dist/client/client/grapesjs/storage.d.ts +2 -0
- package/dist/client/client/grapesjs/symbolDialogs.d.ts +3 -0
- package/dist/client/client/grapesjs/view-buttons.d.ts +1 -0
- package/dist/client/client/index.d.ts +10 -0
- package/dist/client/client/publication-transformers.d.ts +42 -0
- package/dist/client/client/publish-custom-code-block.d.ts +4 -0
- package/dist/client/client/publish-fonts.d.ts +4 -0
- package/dist/client/client/utils.d.ts +22 -0
- package/dist/client/constants.d.ts +32 -0
- package/dist/client/css/admin.css +1114 -328
- package/dist/client/css/admin.css.map +1 -0
- package/dist/client/grapesjs-filter-styles/dist/index.d.ts +4 -0
- package/dist/client/grapesjs-fonts/dist/index.d.ts +4 -0
- package/dist/client/grapesjs-fonts/src/commands.d.ts +3 -0
- package/dist/client/grapesjs-fonts/src/fonts.d.ts +4 -0
- package/dist/client/grapesjs-storage-rate-limit/dist/index.d.ts +4 -0
- package/dist/client/grapesjs-ui-suggest-classes/dist/index.d.ts +4 -0
- package/dist/client/index.html +3 -2
- package/dist/client/js/main.js +356 -131
- package/dist/client/js/main.js.LICENSE.txt +15 -1
- package/dist/client/page.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/client/api.d.ts +90 -0
- package/dist/client/silex-lib/src/ts/client/assetUrl.d.ts +54 -0
- package/dist/client/silex-lib/src/ts/client/config.d.ts +102 -0
- package/dist/client/silex-lib/src/ts/client/events.d.ts +23 -0
- package/dist/client/silex-lib/src/ts/client/expose.d.ts +8 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/LoginDialog.d.ts +15 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationManager.d.ts +73 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationUi.d.ts +59 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/blocks.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/breadcrumbs.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/css-props.d.ts +6 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/footer.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/img.d.ts +6 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/index.d.ts +9 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/internal-links.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/keymaps.d.ts +123 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/new-page-dialog.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/openImport.d.ts +9 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/page-panel.d.ts +9 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/project-bar.d.ts +21 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/resize-panel.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/rich-text.d.ts +4 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/semantic.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/settings-sections.d.ts +16 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/settings.d.ts +4 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/storage.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/symbolDialogs.d.ts +3 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/view-buttons.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/index.d.ts +10 -0
- package/dist/client/silex-lib/src/ts/client/publication-transformers.d.ts +42 -0
- package/dist/client/silex-lib/src/ts/client/publish-custom-code-block.d.ts +4 -0
- package/dist/client/silex-lib/src/ts/client/publish-fonts.d.ts +4 -0
- package/dist/client/silex-lib/src/ts/client/utils.d.ts +22 -0
- package/dist/client/silex-lib/src/ts/constants.d.ts +32 -0
- package/dist/client/silex-lib/src/ts/page.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/types.d.ts +339 -0
- package/dist/client/types.d.ts +339 -0
- package/dist/plugins/client/client/events.d.ts +23 -0
- package/dist/plugins/client/client/events.js +43 -0
- package/dist/plugins/client/client/events.js.map +1 -0
- package/dist/plugins/client/plugins/client/template.d.ts +2 -0
- package/dist/plugins/client/plugins/client/template.js +234 -0
- package/dist/plugins/client/plugins/client/template.js.map +1 -0
- package/dist/plugins/client/plugins/client/website-info.d.ts +1 -0
- package/dist/plugins/client/plugins/client/website-info.js +72 -0
- package/dist/plugins/client/plugins/client/website-info.js.map +1 -0
- package/dist/plugins/client/types.d.ts +339 -0
- package/dist/plugins/client/types.js +80 -0
- package/dist/plugins/client/types.js.map +1 -0
- package/dist/plugins/server/constants.d.ts +32 -0
- package/dist/plugins/server/constants.js +76 -0
- package/dist/plugins/server/constants.js.map +1 -0
- package/dist/plugins/server/plugins/server/Cache.d.ts +2 -0
- package/dist/plugins/server/plugins/server/Cache.js +36 -0
- package/dist/plugins/server/plugins/server/Cache.js.map +1 -0
- package/dist/plugins/server/plugins/server/DownloadConnector.d.ts +29 -0
- package/dist/plugins/server/plugins/server/DownloadConnector.js +127 -0
- package/dist/plugins/server/plugins/server/DownloadConnector.js.map +1 -0
- package/dist/plugins/server/plugins/server/FtpConnector.d.ts +86 -0
- package/dist/plugins/server/plugins/server/FtpConnector.js +536 -0
- package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.d.ts +135 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.js +649 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -0
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.d.ts +16 -0
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js +209 -0
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js.map +1 -0
- package/dist/plugins/server/plugins/server/PublicationOptimizer.d.ts +0 -0
- package/dist/plugins/server/plugins/server/PublicationOptimizer.js +14 -0
- package/dist/plugins/server/plugins/server/PublicationOptimizer.js.map +1 -0
- package/dist/plugins/server/plugins/server/SslPlugin.d.ts +1 -0
- package/dist/plugins/server/plugins/server/SslPlugin.js +86 -0
- package/dist/plugins/server/plugins/server/SslPlugin.js.map +1 -0
- package/dist/plugins/server/plugins/server/StaticPlugin.d.ts +8 -0
- package/dist/plugins/server/plugins/server/StaticPlugin.js +45 -0
- package/dist/plugins/server/plugins/server/StaticPlugin.js.map +1 -0
- package/dist/plugins/server/server/config.d.ts +59 -0
- package/dist/plugins/server/server/config.js +181 -0
- package/dist/plugins/server/server/config.js.map +1 -0
- package/dist/plugins/server/server/connectors/FsHosting.d.ts +14 -0
- package/dist/plugins/server/server/connectors/FsHosting.js +66 -0
- package/dist/plugins/server/server/connectors/FsHosting.js.map +1 -0
- package/dist/plugins/server/server/connectors/FsStorage.d.ts +44 -0
- package/dist/plugins/server/server/connectors/FsStorage.js +261 -0
- package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -0
- package/dist/plugins/server/server/connectors/connectors.d.ts +86 -0
- package/dist/plugins/server/server/connectors/connectors.js +126 -0
- package/dist/plugins/server/server/connectors/connectors.js.map +1 -0
- package/dist/plugins/server/server/events.d.ts +26 -0
- package/dist/plugins/server/server/events.js +31 -0
- package/dist/plugins/server/server/events.js.map +1 -0
- package/dist/plugins/server/server/jobs.d.ts +12 -0
- package/dist/plugins/server/server/jobs.js +79 -0
- package/dist/plugins/server/server/jobs.js.map +1 -0
- package/dist/plugins/server/server/utils/validation.d.ts +7 -0
- package/dist/plugins/server/server/utils/validation.js +37 -0
- package/dist/plugins/server/server/utils/validation.js.map +1 -0
- package/dist/plugins/server/types.d.ts +339 -0
- package/dist/plugins/server/types.js +84 -0
- package/dist/plugins/server/types.js.map +1 -0
- package/dist/server/constants.js +55 -0
- package/dist/server/plugins/server/Cache.js +19 -0
- package/dist/server/server/api/api.js +20 -0
- package/dist/server/server/api/connectorApi.js +334 -0
- package/dist/server/server/api/publicationApi.js +106 -0
- package/dist/server/server/api/websiteApi.js +315 -0
- package/dist/server/server/cli.js +71 -0
- package/dist/server/server/config.js +120 -0
- package/dist/server/server/connectors/FsHosting.js +47 -0
- package/dist/server/server/connectors/FsStorage.js +228 -0
- package/dist/server/server/connectors/connectors.js +88 -0
- package/dist/server/server/events.js +14 -0
- package/dist/server/server/expose.js +27 -0
- package/dist/server/server/express.js +46 -0
- package/dist/server/server/index.js +92 -0
- package/dist/server/server/jobs.js +50 -0
- package/dist/server/server/utils/validation.js +14 -0
- package/dist/server/types.js +40 -0
- package/package.json +99 -49
- package/public/assets/download.png +0 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/favicon.png +0 -0
- package/public/assets/ftp.png +0 -0
- package/public/assets/gitlab.png +0 -0
- package/public/assets/gitlab2.png +0 -0
- package/public/assets/laptop.png +0 -0
- package/public/assets/logo-silex-small.png +0 -0
- package/public/silex.js +22 -23
- package/src/css/_variables.scss +24 -20
- package/src/css/dialog.scss +35 -2
- package/src/css/fonts.scss +31 -0
- package/src/css/footer.scss +12 -0
- package/src/css/grapesjs-light.scss +180 -0
- package/src/css/grapesjs-plugins.scss +227 -0
- package/src/css/grapesjs.scss +159 -1
- package/src/css/index.scss +12 -1
- package/src/css/light.scss +7 -0
- package/src/css/loader.scss +2 -0
- package/src/css/project-bar.scss +32 -16
- package/src/css/silex.scss +79 -5
- package/src/css/website-info.scss +14 -0
- package/src/html/index.pug +3 -3
- package/src/html/templates/includes.pug +0 -1
- package/src/ts/client/api.ts +156 -0
- package/src/ts/client/assetUrl.test.ts +164 -0
- package/src/ts/client/assetUrl.ts +254 -0
- package/src/ts/client/config.ts +175 -13
- package/src/ts/client/events.ts +43 -0
- package/src/ts/client/expose.ts +27 -1
- package/src/ts/client/grapesjs/LoginDialog.ts +204 -0
- package/src/ts/client/grapesjs/PublicationManager.ts +433 -0
- package/src/ts/client/grapesjs/PublicationUi.ts +366 -0
- package/src/ts/client/grapesjs/blocks.ts +17 -0
- package/src/ts/client/grapesjs/breadcrumbs.ts +79 -0
- package/src/ts/client/grapesjs/css-props.ts +910 -0
- package/src/ts/client/grapesjs/eleventy.ts +30 -7
- package/src/ts/client/grapesjs/footer.ts +42 -0
- package/src/ts/client/grapesjs/img.ts +147 -0
- package/src/ts/client/grapesjs/index.ts +311 -122
- package/src/ts/client/grapesjs/internal-links.test.ts +19 -1
- package/src/ts/client/grapesjs/internal-links.ts +31 -7
- package/src/ts/client/grapesjs/keymaps.ts +207 -0
- package/src/ts/client/grapesjs/new-page-dialog.ts +19 -3
- package/src/ts/client/grapesjs/openImport.ts +22 -0
- package/src/ts/client/grapesjs/page-panel.ts +109 -7
- package/src/ts/client/grapesjs/project-bar.ts +133 -72
- package/src/ts/client/grapesjs/resize-panel.ts +20 -0
- package/src/ts/client/grapesjs/rich-text.ts +38 -13
- package/src/ts/client/grapesjs/semantic.ts +20 -3
- package/src/ts/client/grapesjs/settings-sections.ts +117 -0
- package/src/ts/client/grapesjs/settings.ts +191 -96
- package/src/ts/client/grapesjs/storage.ts +187 -0
- package/src/ts/client/grapesjs/symbolDialogs.ts +21 -5
- package/src/ts/client/grapesjs/view-buttons.ts +42 -0
- package/src/ts/client/index.ts +80 -21
- package/src/ts/client/publication-transformers.test.ts +207 -0
- package/src/ts/client/publication-transformers.ts +280 -0
- package/src/ts/client/publish-custom-code-block.ts +31 -0
- package/src/ts/client/publish-fonts.ts +36 -0
- package/src/ts/client/utils.ts +70 -1
- package/src/ts/constants.ts +78 -0
- package/src/ts/externs.ts +17 -0
- package/src/ts/page.ts +20 -3
- package/src/ts/plugins/client/template.ts +246 -0
- package/src/ts/plugins/client/website-info.ts +84 -0
- package/src/ts/plugins/server/Cache.ts +32 -0
- package/src/ts/plugins/server/DownloadConnector.ts +146 -0
- package/src/ts/plugins/server/FtpConnector.test.ts +82 -0
- package/src/ts/plugins/server/FtpConnector.ts +621 -0
- package/src/ts/plugins/server/GitlabConnector.ts +749 -0
- package/src/ts/plugins/server/GitlabHostingConnector.ts +215 -0
- package/src/ts/plugins/server/PublicationOptimizer.ts +14 -0
- package/src/ts/plugins/server/SslPlugin.ts +84 -0
- package/src/ts/plugins/server/StaticPlugin.ts +45 -0
- package/src/ts/server/api/api.ts +19 -0
- package/src/ts/server/api/connectorApi.ts +464 -0
- package/src/ts/server/api/publicationApi.ts +159 -0
- package/src/ts/server/api/websiteApi.ts +462 -0
- package/src/ts/server/cli.ts +48 -14
- package/src/ts/server/config.ts +158 -60
- package/src/ts/server/connectors/FsHosting.ts +66 -0
- package/src/ts/server/connectors/FsStorage.test.ts +51 -0
- package/src/ts/server/connectors/FsStorage.ts +290 -0
- package/src/ts/server/connectors/connectors.ts +203 -0
- package/src/ts/server/events.ts +37 -0
- package/src/ts/server/expose.ts +19 -0
- package/src/ts/server/express.ts +57 -5
- package/src/ts/server/index.ts +80 -19
- package/src/ts/server/jobs.test.ts +75 -0
- package/src/ts/server/jobs.ts +89 -0
- package/src/ts/server/utils/BackwardCompat.ts +546 -528
- package/src/ts/server/utils/validation.ts +34 -0
- package/src/ts/types.ts +295 -85
- package/.dockerignore +0 -5
- package/.editorconfig +0 -14
- package/.eslintrc.json +0 -43
- package/.github/FUNDING.yml +0 -13
- package/.github/workflows/docker-hub-publish.yml +0 -36
- package/.github/workflows/npm-publish.yml +0 -19
- package/.github/workflows/test.yml +0 -26
- package/.nvmrc +0 -1
- package/.silex/default/.silex.data.json +0 -1
- package/CODE_OF_CONDUCT.md +0 -128
- package/CONTRIBUTING.md +0 -131
- package/Dockerfile +0 -15
- package/directus.silex.js +0 -7
- package/dist/constants.js +0 -4
- package/dist/events.js +0 -5
- package/dist/page.js +0 -15
- package/dist/server/cli.js +0 -43
- package/dist/server/config.js +0 -70
- package/dist/server/express.js +0 -18
- package/dist/server/index.js +0 -58
- package/dist/types.js +0 -34
- package/jest.config.js +0 -10
- package/logos/logo_by_ceubri/silex-icon-2018@200px.png +0 -0
- package/logos/logo_by_ceubri/silex-icon-2018@4x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white-transp.pdf +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white.pdf +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@0.5x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@0.75x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@4x.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018-white@4x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018.ai +5 -2521
- package/logos/logo_by_ceubri/silex-logo-2018.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018.pdf +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@0.5x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@0.75x.png +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@4x.jpg +0 -0
- package/logos/logo_by_ceubri/silex-logo-2018@4x.png +0 -0
- package/logos/silexlabs.png +0 -0
- package/src/example-client-config.js +0 -47
- package/src/plugins/AuthPlugin.js +0 -21
- package/src/plugins/Cache.js +0 -11
- package/src/plugins/DefaultBackend.js +0 -149
- package/src/plugins/ExpressPlugin.js +0 -39
- package/src/plugins/HooksPlugin.js +0 -58
- package/src/plugins/PublishPlugin.js +0 -69
- package/src/plugins/SslPlugin.js +0 -66
- package/src/plugins/StaticPlugin.js +0 -48
- package/src/plugins/WebsitePlugin.js +0 -207
- package/src/ts/client/grapesjs/publish.ts +0 -386
- package/src/ts/client/grapesjs/template.ts +0 -166
- package/src/ts/events.ts +0 -2
- package/tsconfig-server-debug.json +0 -19
- package/tsconfig-server-release.json +0 -19
- package/tsconfig.jest.json +0 -9
- package/tsconfig.json +0 -26
- package/webpack.config.js +0 -28
- package/webpack.debug.config.js +0 -8
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
3
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { html, render, TemplateResult } from 'lit-html'
|
|
19
|
+
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
|
|
20
|
+
//import { map } from 'lit-html/directives/map.js'
|
|
21
|
+
import { cmdPublicationLogin, cmdPublicationLogout, cmdPublicationStart, PublicationStatus, PublishableEditor } from './PublicationManager'
|
|
22
|
+
import { ConnectorData, ConnectorType, PublicationJobData, PublicationSettings } from '../../types'
|
|
23
|
+
import { connectorList } from '../api'
|
|
24
|
+
import { defaultKms } from './keymaps'
|
|
25
|
+
import { titleCase } from '../utils'
|
|
26
|
+
import { ClientEvent } from '../events'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @fileoverview define the publication dialog
|
|
30
|
+
* This is the UI of the publication feature
|
|
31
|
+
* It is a dialog which allows the user to login to a connector and publish the website
|
|
32
|
+
* It also displays the publication status and logs during the publication process
|
|
33
|
+
* This class is used by the PublicationManager
|
|
34
|
+
* This is optional, you can use the PublicationManager without this UI
|
|
35
|
+
* @TODO Use publication events instead of calls from the PublicationManager
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// **
|
|
39
|
+
// Constants
|
|
40
|
+
export const cmdPublish = 'publish-open-dialog'
|
|
41
|
+
|
|
42
|
+
// **
|
|
43
|
+
// Semantic Icons
|
|
44
|
+
const svgConnector = '<svg width="100%" height="100%" viewBox="0 0 44 23" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><g><g><path d="M2,11.427c0,0 3.846,-2.655 7.376,0c3.531,2.656 7.45,0 7.45,0" style="fill:none;stroke:#a291ff;stroke-width:4px;"/></g><path d="M32.165,6.424l9.156,0" style="fill:none;stroke:#a291ff;stroke-width:4px;"/><path d="M32.165,2l0,18.854" style="fill:none;stroke:#a291ff;stroke-width:4px;"/><path d="M32.165,16.43l9.156,0" style="fill:none;stroke:#a291ff;stroke-width:4px;"/><path d="M26.253,20.854c-5.203,0 -9.427,-4.224 -9.427,-9.427c-0,-5.203 4.224,-9.427 9.427,-9.427l5.912,-0l0,18.854l-5.912,0Z" style="fill:none;stroke:#a291ff;stroke-width:4px;"/></g></svg>'
|
|
45
|
+
const svgSuccess = '<svg viewBox="0 0 40 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><path d="M3,12.969l11.868,11.869l21.838,-21.838" style="fill:none;stroke:#37bf76;stroke-width:6px;"/></svg>'
|
|
46
|
+
const svgError = '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><g><path d="M3,28.915l25.915,-25.915" style="fill:none;stroke:#cf4443;stroke-width:6px;"/><path d="M28.915,28.915l-25.915,-25.915" style="fill:none;stroke:#cf4443;stroke-width:6px;"/></g></svg>'
|
|
47
|
+
|
|
48
|
+
// **
|
|
49
|
+
// Types
|
|
50
|
+
export type PublicationDialogOptions = {
|
|
51
|
+
appendTo: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// **
|
|
55
|
+
// Utils
|
|
56
|
+
function cleanupLogEntry(arr: string[][]): string {
|
|
57
|
+
return arr[arr.length - 1]
|
|
58
|
+
?.map(str => str.replace(/\[.*\]/g, '').trim())
|
|
59
|
+
?.filter(str => !!str)
|
|
60
|
+
?.join('\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Class to manage the publication dialog
|
|
65
|
+
*/
|
|
66
|
+
export class PublicationUi {
|
|
67
|
+
/**
|
|
68
|
+
* Dialog state
|
|
69
|
+
*/
|
|
70
|
+
public isOpen = false
|
|
71
|
+
/**
|
|
72
|
+
* Dialog content
|
|
73
|
+
*/
|
|
74
|
+
private errorMessage = ''
|
|
75
|
+
/**
|
|
76
|
+
* Dialog element
|
|
77
|
+
* This is the DOM element of the dialog
|
|
78
|
+
*/
|
|
79
|
+
private el: HTMLElement
|
|
80
|
+
|
|
81
|
+
public settings: PublicationSettings
|
|
82
|
+
|
|
83
|
+
private sender = null
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initialize the dialog and the publish button
|
|
87
|
+
*/
|
|
88
|
+
constructor(private editor: PublishableEditor, private options: PublicationDialogOptions) {
|
|
89
|
+
// Reference to the dialog element
|
|
90
|
+
this.el = this.createDialogElements()
|
|
91
|
+
// Add the publish command to the editor
|
|
92
|
+
const openDialog = () => this.openDialog()
|
|
93
|
+
const closeDialog = () => this.closeDialog()
|
|
94
|
+
const setSender = sender => this.sender = sender
|
|
95
|
+
editor.Commands.add(cmdPublish, {
|
|
96
|
+
run(editor: PublishableEditor, sender) {
|
|
97
|
+
setSender(sender)
|
|
98
|
+
openDialog()
|
|
99
|
+
},
|
|
100
|
+
stop(editor: PublishableEditor) { closeDialog() },
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// **
|
|
105
|
+
// Convenient state checkers
|
|
106
|
+
isSuccess(status: PublicationStatus) {
|
|
107
|
+
return status === PublicationStatus.STATUS_SUCCESS
|
|
108
|
+
}
|
|
109
|
+
isPending(status: PublicationStatus) {
|
|
110
|
+
return status === PublicationStatus.STATUS_PENDING
|
|
111
|
+
}
|
|
112
|
+
isError(status: PublicationStatus) {
|
|
113
|
+
return status === PublicationStatus.STATUS_ERROR
|
|
114
|
+
}
|
|
115
|
+
isLoggedOut(status: PublicationStatus) {
|
|
116
|
+
return status === PublicationStatus.STATUS_LOGGED_OUT
|
|
117
|
+
}
|
|
118
|
+
isReady(status: PublicationStatus) {
|
|
119
|
+
return status === PublicationStatus.STATUS_NONE
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// **
|
|
123
|
+
// Functions to open and close the dialog
|
|
124
|
+
userContent: TemplateResult | null = null
|
|
125
|
+
setUserContent(content: TemplateResult) {
|
|
126
|
+
this.userContent = content
|
|
127
|
+
}
|
|
128
|
+
createDialogElements(): HTMLElement {
|
|
129
|
+
// Create the dialog element
|
|
130
|
+
const el = document.createElement('div')
|
|
131
|
+
el.id = 'publish-dialog'
|
|
132
|
+
el.className = 'silex-dialog-inline silex-dialog gjs-two-color'
|
|
133
|
+
document.body.append(el)
|
|
134
|
+
// Let the other buttons be added, we want to be last
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
// Create the publish button
|
|
137
|
+
this.editor.Panels.addButton(this.options.appendTo, {
|
|
138
|
+
id: 'publish-button',
|
|
139
|
+
className: 'silex-button--size publish-button',
|
|
140
|
+
command: cmdPublish,
|
|
141
|
+
attributes: { title: `Publish (${titleCase(defaultKms.kmOpenPublish.keys, '+')})` },
|
|
142
|
+
label: '<span class="fa-solid fa-upload"></span><span class="silex-button--small">Publish</span>',
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
return el
|
|
146
|
+
}
|
|
147
|
+
move(rect) {
|
|
148
|
+
Object.keys(rect).forEach(key => this.el.style[key] = rect[key] + 'px')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// **
|
|
152
|
+
// Functions to render the dialog
|
|
153
|
+
private async renderDialog(job: PublicationJobData, status: PublicationStatus) {
|
|
154
|
+
try {
|
|
155
|
+
if(this.isOpen && (!status || !this.settings)) throw new Error('PublicationUi: open but no status or settings')
|
|
156
|
+
render(html`
|
|
157
|
+
${!this.isOpen ? '' : this.settings.connector ? await this.renderOpenDialog(job, status) : await this.renderLoginDialog(status)}
|
|
158
|
+
`, this.el)
|
|
159
|
+
if (this.isOpen) {
|
|
160
|
+
this.el.classList.remove('silex-dialog-hide')
|
|
161
|
+
const primaryBtn = this.el.querySelector('.silex-button--primary') as HTMLElement
|
|
162
|
+
if (primaryBtn) primaryBtn.focus()
|
|
163
|
+
} else {
|
|
164
|
+
this.el.classList.add('silex-dialog-hide')
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error('Error while rendering the dialog', err)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async renderOpenDialog(job: PublicationJobData, status: PublicationStatus): Promise<TemplateResult> {
|
|
171
|
+
return html`
|
|
172
|
+
<header>
|
|
173
|
+
<span>${unsafeHTML(svgConnector)} Connected to <span class="connector">${this.settings.connector.displayName}</span></span>
|
|
174
|
+
${this.settings.connector.disableLogout ? '' : html`
|
|
175
|
+
<button
|
|
176
|
+
class="silex-button silex-button--secondary"
|
|
177
|
+
id="publish-button--secondary"
|
|
178
|
+
@click=${() => this.editor.Commands.run(cmdPublicationLogout)}
|
|
179
|
+
>Disconnect
|
|
180
|
+
</button>`}
|
|
181
|
+
</header>
|
|
182
|
+
<main>
|
|
183
|
+
${this.userContent || html`
|
|
184
|
+
${this.isPending(status) ? html`
|
|
185
|
+
<p>Publication in progress...</p>
|
|
186
|
+
` : ''}
|
|
187
|
+
${this.isReady(status) ? html`
|
|
188
|
+
<p>Click on the button below to publish your website.</p>
|
|
189
|
+
${this.settings.options && Object.entries(this.settings.options).length && html`<p>Publication options: <ul>${ Object.entries(this.settings.options).map(([key, value]) => html`<li>${key}: ${value}</li>`) }</ul></p>`}
|
|
190
|
+
` : ''}
|
|
191
|
+
${this.isSuccess(status) ? html`
|
|
192
|
+
<h3 class="status">Publication success ${unsafeHTML(svgSuccess)}</h3>
|
|
193
|
+
${this.settings.options?.websiteUrl ? html`<p><a href="${this.settings.options.websiteUrl}" target="_blank">Click here to view the published website</a></p>` : ''}
|
|
194
|
+
` : ''}
|
|
195
|
+
${this.isError(status) || this.isLoggedOut(status) ? html`
|
|
196
|
+
<h3 class="status">Publication error ${unsafeHTML(svgError)}</h3>
|
|
197
|
+
<details open>
|
|
198
|
+
<summary>Details</summary>
|
|
199
|
+
${unsafeHTML(this.errorMessage)}
|
|
200
|
+
</details>
|
|
201
|
+
` : ''}
|
|
202
|
+
${job?.message ? html`
|
|
203
|
+
<details open>
|
|
204
|
+
<summary>Details</summary>
|
|
205
|
+
${unsafeHTML(job.message)}
|
|
206
|
+
</details>
|
|
207
|
+
` : ''}
|
|
208
|
+
${this.isPending(status) ? html`
|
|
209
|
+
<progress
|
|
210
|
+
value=""
|
|
211
|
+
style="width: 100%;"
|
|
212
|
+
></progress>
|
|
213
|
+
` : ''}
|
|
214
|
+
${job?.logs?.length && job.logs[0].length ? html`
|
|
215
|
+
<details>
|
|
216
|
+
<summary>Logs</summary>
|
|
217
|
+
<div class="logs">${unsafeHTML(cleanupLogEntry(job.logs))}</div>
|
|
218
|
+
</details>
|
|
219
|
+
` : ''}
|
|
220
|
+
${job?.errors?.length && job.errors[0].length ? html`
|
|
221
|
+
<details>
|
|
222
|
+
<summary>Errors</summary>
|
|
223
|
+
<pre style="
|
|
224
|
+
max-width: 100%;
|
|
225
|
+
font-size: x-small;
|
|
226
|
+
"
|
|
227
|
+
>${unsafeHTML(cleanupLogEntry(job.errors))}
|
|
228
|
+
</pre>
|
|
229
|
+
</details>
|
|
230
|
+
` : ''}
|
|
231
|
+
${this.isPending(status) || this.isLoggedOut(status) ? '' : html`
|
|
232
|
+
<button
|
|
233
|
+
class="silex-button silex-button--primary"
|
|
234
|
+
id="publish-button--primary"
|
|
235
|
+
@click=${() => this.editor.Commands.run(cmdPublicationStart)}
|
|
236
|
+
>Publish</button>
|
|
237
|
+
`}
|
|
238
|
+
`}
|
|
239
|
+
</main>
|
|
240
|
+
<footer>
|
|
241
|
+
${this.isLoggedOut(status) ? html`
|
|
242
|
+
<button
|
|
243
|
+
class="silex-button silex-button--primary"
|
|
244
|
+
id="publish-button--primary"
|
|
245
|
+
@click=${() => this.editor.Commands.run(cmdPublicationLogin, this.settings.connector)}
|
|
246
|
+
>Connect</button>
|
|
247
|
+
`: ''}
|
|
248
|
+
<a href="https://docs.silex.me/en/user/publish" target="_blank">Help</a>
|
|
249
|
+
<button
|
|
250
|
+
class="silex-button silex-button--secondary"
|
|
251
|
+
id="publish-button--secondary"
|
|
252
|
+
@click=${() => this.editor.Commands.stop(cmdPublish)}
|
|
253
|
+
>Close</button>
|
|
254
|
+
</footer>
|
|
255
|
+
`
|
|
256
|
+
}
|
|
257
|
+
async renderLoginDialog(status: PublicationStatus): Promise<TemplateResult> {
|
|
258
|
+
try {
|
|
259
|
+
render(html`<main><p>Loading</p></main>`, this.el)
|
|
260
|
+
const hostingConnectors = await connectorList({ type: ConnectorType.HOSTING })
|
|
261
|
+
const loggedConnectors: ConnectorData[] = hostingConnectors.filter(connector => connector.isLoggedIn)
|
|
262
|
+
if (hostingConnectors.length === 1 && loggedConnectors.length === 1) {
|
|
263
|
+
this.settings.connector = loggedConnectors[0]
|
|
264
|
+
return this.renderOpenDialog(null, PublicationStatus.STATUS_NONE)
|
|
265
|
+
}
|
|
266
|
+
//const loggedConnector: ConnectorData = hostingConnectors.find(connector => connector.isLoggedIn)
|
|
267
|
+
//if (loggedConnector) {
|
|
268
|
+
// this.settings.connector = loggedConnector
|
|
269
|
+
// return this.renderOpenDialog(null, PublicationStatus.STATUS_NONE)
|
|
270
|
+
//}
|
|
271
|
+
return html`
|
|
272
|
+
<main>
|
|
273
|
+
<p>You need to select a hosting connector to publish your website.</p>
|
|
274
|
+
${this.isError(status) || this.isLoggedOut(status) ? html`
|
|
275
|
+
<p>Login error</p>
|
|
276
|
+
<div>${unsafeHTML(this.errorMessage)}</div>
|
|
277
|
+
` : ''}
|
|
278
|
+
<div class="buttons">
|
|
279
|
+
${hostingConnectors.map(connector => html`
|
|
280
|
+
<button
|
|
281
|
+
class="silex-button"
|
|
282
|
+
id="publish-button--primary"
|
|
283
|
+
@click=${() => this.editor.Commands.run(cmdPublicationLogin, connector)}
|
|
284
|
+
>${connector.displayName}</button>
|
|
285
|
+
`)}
|
|
286
|
+
</div>
|
|
287
|
+
</main>
|
|
288
|
+
<footer>
|
|
289
|
+
<a href="https://docs.silex.me/en/user/publish" target="_blank">Help</a>
|
|
290
|
+
<button
|
|
291
|
+
class="silex-button silex-button--secondary"
|
|
292
|
+
id="publish-button--secondary"
|
|
293
|
+
@click=${() => this.editor.Commands.stop(cmdPublish)}
|
|
294
|
+
>Close</button>
|
|
295
|
+
</footer>
|
|
296
|
+
`
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error(err)
|
|
299
|
+
return html`
|
|
300
|
+
<header>
|
|
301
|
+
<h3>Oops</h3>
|
|
302
|
+
</header>
|
|
303
|
+
<main>
|
|
304
|
+
<p>Unable to load hosting connectors</p>
|
|
305
|
+
<p>Something went wrong: ${err.message}</p>
|
|
306
|
+
</main>
|
|
307
|
+
<footer>
|
|
308
|
+
<a href="https://docs.silex.me/en/user/publish" target="_blank">Help</a>
|
|
309
|
+
<button
|
|
310
|
+
class="silex-button silex-button--secondary"
|
|
311
|
+
id="publish-button--secondary"
|
|
312
|
+
@click=${() => this.editor.Commands.stop(cmdPublish)}
|
|
313
|
+
>Close</button>
|
|
314
|
+
</footer>
|
|
315
|
+
`
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
displayPending(job: PublicationJobData, status: PublicationStatus) {
|
|
319
|
+
this.errorMessage = null
|
|
320
|
+
this.renderDialog(job, status)
|
|
321
|
+
}
|
|
322
|
+
displayError(message: string, job: PublicationJobData, status: PublicationStatus) {
|
|
323
|
+
console.info(message)
|
|
324
|
+
this.errorMessage = message
|
|
325
|
+
this.renderDialog(job, status)
|
|
326
|
+
}
|
|
327
|
+
async closeDialog() {
|
|
328
|
+
this.isOpen = false
|
|
329
|
+
this.renderDialog(null, null)
|
|
330
|
+
if (this.sender && typeof this.sender.set === 'function') {
|
|
331
|
+
this.sender.set('active', 0)
|
|
332
|
+
} // Deactivate the button to make it ready to be clicked again
|
|
333
|
+
this.sender = null
|
|
334
|
+
this.editor.trigger(ClientEvent.PUBLICATION_UI_CLOSE, { publicationUi: this })
|
|
335
|
+
}
|
|
336
|
+
async toggleDialog() {
|
|
337
|
+
if (this.isOpen) this.closeDialog()
|
|
338
|
+
else this.openDialog()
|
|
339
|
+
}
|
|
340
|
+
async openDialog() {
|
|
341
|
+
this.isOpen = true
|
|
342
|
+
|
|
343
|
+
// Position
|
|
344
|
+
const buttonEl = this.editor.Panels.getPanel('options')?.view.el.querySelector('.publish-button')
|
|
345
|
+
if(buttonEl) {
|
|
346
|
+
const rect = buttonEl.getBoundingClientRect()
|
|
347
|
+
|
|
348
|
+
const width = 450
|
|
349
|
+
const padding = 10 * 2
|
|
350
|
+
const minHeight = 50
|
|
351
|
+
this.move({
|
|
352
|
+
left: rect.right - width - padding,
|
|
353
|
+
top: rect.bottom + 10,
|
|
354
|
+
width,
|
|
355
|
+
minHeight,
|
|
356
|
+
})
|
|
357
|
+
} else {
|
|
358
|
+
console.error('Unable to find publish button')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Publication
|
|
362
|
+
//this.editor.Commands.run(cmdPublicationStart)
|
|
363
|
+
this.renderDialog(null, PublicationStatus.STATUS_NONE)
|
|
364
|
+
this.editor.trigger(ClientEvent.PUBLICATION_UI_OPEN, { publicationUi: this })
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
3
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
1
18
|
export const blocksPlugin = (editor, opts) => {
|
|
2
19
|
// Defaults
|
|
3
20
|
const options = {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { onFooter } from './footer'
|
|
2
|
+
|
|
3
|
+
export default function (editor, options) {
|
|
4
|
+
let breadcrumbsContainer
|
|
5
|
+
onFooter(footer => {
|
|
6
|
+
// Initialize the breadcrumbs container
|
|
7
|
+
breadcrumbsContainer = document.createElement('div')
|
|
8
|
+
breadcrumbsContainer.id = 'breadcrumbs-container'
|
|
9
|
+
footer.prepend(breadcrumbsContainer)
|
|
10
|
+
// Add breadcrumbs styles
|
|
11
|
+
const breadcrumbsStyles = document.createElement('style')
|
|
12
|
+
breadcrumbsStyles.innerHTML = `
|
|
13
|
+
#breadcrumbs-container {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
height: 100%;
|
|
17
|
+
padding: 0 10px;
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
color: #999;
|
|
20
|
+
overflow-x: auto;
|
|
21
|
+
overflow-y: hidden;
|
|
22
|
+
}
|
|
23
|
+
#breadcrumbs-container h3 {
|
|
24
|
+
font-size: inherit;
|
|
25
|
+
margin: 0;
|
|
26
|
+
}
|
|
27
|
+
#breadcrumbs-container .breadcrumb {
|
|
28
|
+
margin: 0 5px;
|
|
29
|
+
font-size: 14px;
|
|
30
|
+
line-height: 1;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
}
|
|
34
|
+
#breadcrumbs-container .breadcrumb::after {
|
|
35
|
+
content: "➔";
|
|
36
|
+
margin-left: 10px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#breadcrumbs-container .breadcrumb:last-child::after {
|
|
40
|
+
content: "";
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
footer.prepend(breadcrumbsStyles)
|
|
44
|
+
renderBreadcrumbs()
|
|
45
|
+
})
|
|
46
|
+
// Append the breadcrumbs container to the editor's container
|
|
47
|
+
|
|
48
|
+
editor.on('component:selected style:target component:drag:end', () => renderBreadcrumbs())
|
|
49
|
+
function renderBreadcrumbs() {
|
|
50
|
+
let component = editor.getSelected() ?? editor.Pages.getSelected().getMainComponent()
|
|
51
|
+
|
|
52
|
+
if(!breadcrumbsContainer) return
|
|
53
|
+
|
|
54
|
+
// Clear the breadcrumbs container
|
|
55
|
+
breadcrumbsContainer.innerHTML = ''
|
|
56
|
+
|
|
57
|
+
// Traverse up the tree of components, prepending each to the breadcrumbs
|
|
58
|
+
while (component) {
|
|
59
|
+
const breadcrumb = createBreadcrumb(component)
|
|
60
|
+
breadcrumbsContainer.prepend(breadcrumb)
|
|
61
|
+
component = component.parent()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Label
|
|
65
|
+
const label = document.createElement('span')
|
|
66
|
+
label.innerHTML = '<h3>Selection: </h3>'
|
|
67
|
+
breadcrumbsContainer.prepend(label)
|
|
68
|
+
}
|
|
69
|
+
function createBreadcrumb(component) {
|
|
70
|
+
const breadcrumb = document.createElement('span')
|
|
71
|
+
const model = component.model
|
|
72
|
+
breadcrumb.onclick = () => {
|
|
73
|
+
editor.select(component)
|
|
74
|
+
}
|
|
75
|
+
breadcrumb.classList.add('breadcrumb')
|
|
76
|
+
breadcrumb.innerHTML = `<span>${component.getName() ?? component.get('tagName')}</span>`
|
|
77
|
+
return breadcrumb
|
|
78
|
+
}
|
|
79
|
+
}
|