diff --git a/bundles/org.openhab.ui/web/package-lock.json b/bundles/org.openhab.ui/web/package-lock.json index 8e1d591893..25dbecd5d3 100644 --- a/bundles/org.openhab.ui/web/package-lock.json +++ b/bundles/org.openhab.ui/web/package-lock.json @@ -89,6 +89,7 @@ "jest-serializer-vue": "^2.0.2", "jest-transform-stub": "^2.0.0", "jest-vue-preprocessor": "^1.7.1", + "marked": "^3.0.2", "mini-css-extract-plugin": "^0.5.0", "nearley-loader": "^2.0.0", "optimize-css-assets-webpack-plugin": "^5.0.4", @@ -14207,6 +14208,18 @@ "node": ">=0.10.0" } }, + "node_modules/marked": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.2.tgz", + "integrity": "sha512-TMJQQ79Z0e3rJYazY0tIoMsFzteUGw9fB3FD+gzuIT3zLuG9L9ckIvUfF51apdJkcqc208jJN2KbtPbOvXtbjA==", + "dev": true, + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -35347,6 +35360,12 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.2.tgz", + "integrity": "sha512-TMJQQ79Z0e3rJYazY0tIoMsFzteUGw9fB3FD+gzuIT3zLuG9L9ckIvUfF51apdJkcqc208jJN2KbtPbOvXtbjA==", + "dev": true + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/bundles/org.openhab.ui/web/package.json b/bundles/org.openhab.ui/web/package.json index ad553b5f8d..cbff3656a8 100644 --- a/bundles/org.openhab.ui/web/package.json +++ b/bundles/org.openhab.ui/web/package.json @@ -141,6 +141,7 @@ "jest-serializer-vue": "^2.0.2", "jest-transform-stub": "^2.0.0", "jest-vue-preprocessor": "^1.7.1", + "marked": "^3.0.2", "mini-css-extract-plugin": "^0.5.0", "nearley-loader": "^2.0.0", "optimize-css-assets-webpack-plugin": "^5.0.4", diff --git a/bundles/org.openhab.ui/web/src/assets/addon-store.js b/bundles/org.openhab.ui/web/src/assets/addon-store.js new file mode 100644 index 0000000000..bc1b4a228a --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/addon-store.js @@ -0,0 +1,66 @@ +export const AddonIcons = { + automation: 'wand_stars', + binding: 'circle_grid_hex_fill', + persistence: 'download_circle', + transformation: 'function', + misc: 'rectangle_3_offgrid', + ui: 'play_rectangle', + voice: 'chat_bubble_2_fill' +} + +export const ContentTypes = { + 'application/java-archive': 'Java Archive', + 'application/vnd.openhab.bundle': 'OSGi Bundle', + 'application/vnd.openhab.feature;type=karaf': 'Karaf Feature', + 'application/vnd.openhab.ruletemplate': 'Rule Template', + 'application/vnd.openhab.uicomponent;type=widget': 'UI Component - Widget' +} + +export const Formats = { + 'yaml_content': 'Inline YAML Code', + 'json_content': 'Inline JSON Code', + 'yaml_download_url': 'Linked YAML File', + 'json_download_url': 'Linked JSON File', + 'jar_download_url': 'Linked JAR file', + 'kar_download_url': 'Linked KAR file', + 'karaf': 'Karaf' +} + +export const AddonStoreTabShortcuts = [ + { + id: 'bindings', + label: 'Bindings', + icon: 'circle_grid_hex', + subtitle: 'Connect and control hardware and online services' + }, + { + id: 'automation', + label: 'Automation', + icon: 'sparkles', + subtitle: 'Scripting languages, templates and module types' + }, + { + id: 'ui', + label: 'User Interfaces', + icon: 'play_rectangle', + subtitle: 'Community widgets & alternative frontends' + }, + { + id: 'other', + label: 'Other Add-ons', + icon: 'ellipsis', + subtitle: 'System integrations, persistence, voice & more' + } +] + +export function compareAddons (a1, a2) { + if (a1.installed && !a2.installed) return -1 + if (a2.installed && !a1.installed) return 1 + if (a1.verifiedAuthor && !a2.verifiedAuthor) return -1 + if (a2.verifiedAuthor && !a1.verifiedAuthor) return 1 + if (a1.properties && a2.properties && a1.properties.like_count >= 0 && a2.properties.like_count >= 0) return (a1.properties.like_count > a2.properties.like_count) ? 1 : -1 + if (a1.properties && a2.properties && a1.properties.views >= 0 && a2.properties.views >= 0) return (a1.properties.views > a2.properties.views) ? 1 : -1 + const nameOrId1 = a1.label || a1.id + const nameOrId2 = a2.label || a2.name + return nameOrId1.localeCompare(nameOrId2) +} diff --git a/bundles/org.openhab.ui/web/src/components/addons/addon-card.vue b/bundles/org.openhab.ui/web/src/components/addons/addon-card.vue new file mode 100644 index 0000000000..3bd465a29d --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/addons/addon-card.vue @@ -0,0 +1,148 @@ + + + + {{ headline || autoHeadline }} + + + + + + + + + {{ addon.label }} + + + {{ addon.author }} + + + + + + + + + + + + + diff --git a/bundles/org.openhab.ui/web/src/components/addons/addon-list-item.vue b/bundles/org.openhab.ui/web/src/components/addons/addon-list-item.vue new file mode 100644 index 0000000000..c75ab67633 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/addons/addon-list-item.vue @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.ui/web/src/components/addons/addons-section.vue b/bundles/org.openhab.ui/web/src/components/addons/addons-section.vue new file mode 100644 index 0000000000..8bd51ece9a --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/addons/addons-section.vue @@ -0,0 +1,147 @@ + + + + {{ title }} + + See All + + + + {{ subtitle }} + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.ui/web/src/components/app.vue b/bundles/org.openhab.ui/web/src/components/app.vue index af75efaa2d..1e764f5f01 100644 --- a/bundles/org.openhab.ui/web/src/components/app.vue +++ b/bundles/org.openhab.ui/web/src/components/app.vue @@ -13,7 +13,6 @@ - {{ $t('sidebar.noPages') }} diff --git a/bundles/org.openhab.ui/web/src/css/app.styl b/bundles/org.openhab.ui/web/src/css/app.styl index 37d74e9931..b2c344127a 100644 --- a/bundles/org.openhab.ui/web/src/css/app.styl +++ b/bundles/org.openhab.ui/web/src/css/app.styl @@ -255,6 +255,8 @@ html #framework7-root:not(.theme-dark) .page-home:not(.standard-background), #framework7-root:not(.theme-dark) .page-about:not(.standard-background), +#framework7-root:not(.theme-dark) .page-addon-store:not(.standard-background), +#framework7-root:not(.theme-dark) .page-addon-details:not(.standard-background), //#framework7-root:not(.theme-dark) .page-settings --f7-page-bg-color #fff diff --git a/bundles/org.openhab.ui/web/src/js/routes.js b/bundles/org.openhab.ui/web/src/js/routes.js index 5734e8dd35..48e5d461d2 100644 --- a/bundles/org.openhab.ui/web/src/js/routes.js +++ b/bundles/org.openhab.ui/web/src/js/routes.js @@ -11,6 +11,8 @@ const ServiceSettingsPage = () => import(/* webpackChunkName: "admin-base" */ '. const AddonsListPage = () => import(/* webpackChunkName: "admin-base" */ '../pages/settings/addons/addons-list.vue') const AddonsAddPage = () => import(/* webpackChunkName: "admin-base" */ '../pages/settings/addons/addons-add.vue') const AddonsConfigureBindingPage = () => import(/* webpackChunkName: "admin-base" */ '../pages/settings/addons/binding-config.vue') +const AddonsStorePage = () => import(/* webpackChunkName: "admin-base" */ '../pages/settings/addons/addons-store.vue') +const AddonDetailsPage = () => import(/* webpackChunkName: "admin-base" */ '../pages/settings/addons/addon-details.vue') const ItemsListPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/items/items-list-vlist.vue') const ItemDetailsPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/items/item-details.vue') @@ -231,12 +233,12 @@ export default [ ] }, { - path: 'addons/:addonType', - async: loadAsync(AddonsListPage), + path: 'addons', + async: loadAsync(AddonsStorePage), routes: [ { - path: 'add', - async: loadAsync(AddonsAddPage) + path: ':addonId', + async: loadAsync(AddonDetailsPage) }, { path: ':bindingId/config', diff --git a/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-details-sheet.vue b/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-details-sheet.vue index cac92ac229..49c49da888 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-details-sheet.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-details-sheet.vue @@ -30,8 +30,8 @@ - - + + @@ -93,7 +93,7 @@ diff --git a/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-store-mixin.js b/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-store-mixin.js new file mode 100644 index 0000000000..83a4c47e55 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/pages/settings/addons/addon-store-mixin.js @@ -0,0 +1,84 @@ +import AddonDetailsSheet from './addon-details-sheet.vue' + +export default { + components: { + AddonDetailsSheet + }, + data () { + return { + addons: {}, + currentAddon: null, + currentAddonId: null, + currentServiceId: null, + ready: false, + initSearchbar: false, + addonPopupOpened: false, + currentlyInstalling: [], + currentlyUninstalling: [] + } + }, + methods: { + openAddonPopup (addonId, serviceId, addon) { + this.currentAddonId = addonId + this.currentServiceId = serviceId + if (addon) this.currentAddon = addon + this.addonPopupOpened = true + }, + installAddon (addon) { + this.addonPopupOpened = false + this.currentlyInstalling.push(addon.id) + if (this.currentAddon) this.$set(this.currentAddon, 'pending', 'INSTALL') + }, + uninstallAddon (addon) { + this.addonPopupOpened = false + this.currentlyUninstalling.push(addon.id) + if (this.currentAddon) this.$set(this.currentAddon, 'pending', 'UNINSTALL') + }, + isInstalling (addon) { + return this.currentlyInstalling.indexOf(addon.id) >= 0 + }, + isUninstalling (addon) { + return this.currentlyUninstalling.indexOf(addon.id) >= 0 + }, + isPending (addon) { + return this.isInstalling(addon) || this.isUninstalling(addon) + }, + resetPending () { + this.$set(this, 'currentlyInstalling', []) + this.$set(this, 'currentlyUninstalling', []) + this.$set(this, 'currentAddon', null) + this.currentAddonId = null + this.currentServiceId = null + }, + startEventSource () { + this.eventSource = this.$oh.sse.connect('/rest/events?topics=openhab/addons/*/*', null, (event) => { + const topicParts = event.topic.split('/') + switch (topicParts[3]) { + case 'installed': + case 'uninstalled': + this.stopEventSource() + this.load() + this.$f7.emit('addonChange', null) + break + case 'failed': + this.$f7.toast.create({ + text: `Installation of add-on ${topicParts[2]} failed`, + closeButton: true, + destroyOnClose: true + }).open() + this.stopEventSource() + this.load() + break + } + }, () => { + // in case of error, maybe the SSE connection was closed by the add-ons change itself - try reloading to refresh + this.stopEventSource() + this.load() + }) + }, + stopEventSource () { + this.$oh.sse.close(this.eventSource) + this.eventSource = null + } + } +} diff --git a/bundles/org.openhab.ui/web/src/pages/settings/addons/addons-store.vue b/bundles/org.openhab.ui/web/src/pages/settings/addons/addons-store.vue new file mode 100644 index 0000000000..7b870e51fe --- /dev/null +++ b/bundles/org.openhab.ui/web/src/pages/settings/addons/addons-store.vue @@ -0,0 +1,230 @@ + + + + + Bindings + Automation + User Interfaces + Other Add-ons + Search + + + Bindings + Automation + User Interfaces + Other Add-ons + Search + + + + + + + + + + + + + + + + + Loading... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.ui/web/src/pages/settings/settings-menu.vue b/bundles/org.openhab.ui/web/src/pages/settings/settings-menu.vue index d3ccbae26f..6a9ce42f4a 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/settings-menu.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/settings-menu.vue @@ -16,7 +16,7 @@ search-in=".item-title" :disable-button="!$theme.aurora" /> - + Configuration @@ -90,19 +90,20 @@ - + Add-ons - + v-for="shortcut in addonStoreTabShortcuts" + :key="shortcut.id" + :link="true" + @click="navigateToStore(shortcut.id)" + :title="shortcut.label" + :footer="shortcut.subtitle"> + @@ -133,12 +134,14 @@