diff --git a/platypush/backend/http/webapp/src/components/File/Browser.vue b/platypush/backend/http/webapp/src/components/File/Browser.vue index dc41e1db39..4e7eabb1ca 100644 --- a/platypush/backend/http/webapp/src/components/File/Browser.vue +++ b/platypush/backend/http/webapp/src/components/File/Browser.vue @@ -134,6 +134,7 @@ export default { try { this.files = await this.request('file.list', {path: this.path}) this.$emit('path-change', this.path) + this.setUrlArgs({path: decodeURIComponent(this.path)}) } finally { this.loading = false } @@ -165,8 +166,16 @@ export default { }, mounted() { + const args = this.getUrlArgs() + if (args.path) + this.path = args.path + this.refresh() }, + + unmounted() { + this.setUrlArgs({path: null}) + }, } diff --git a/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue b/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue index b1c82d2f84..19ebf3b74e 100644 --- a/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue +++ b/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue @@ -1,5 +1,5 @@ @@ -490,4 +798,10 @@ export default { } } } + +:deep(.add-to-playlist-container) { + .body { + padding: 0 !important; + } +} diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Info.vue b/platypush/backend/http/webapp/src/components/panels/Media/Info.vue index 09ac0ed28c..135de2e251 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Info.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Info.vue @@ -2,7 +2,7 @@
- +
@@ -16,6 +16,18 @@
+
+
Direct URL
+
+ + + + +
+
+
TV Series
@@ -169,12 +181,18 @@ export default { item: { type: Object, default: () => {}, - } + }, + + pluginName: { + type: String, + }, }, data() { return { typeIcons: Icons, + loadingUrl: false, + youtubeUrl: null, } }, @@ -209,6 +227,15 @@ export default { return null }, + + directUrl() { + if (this.item?.type === 'file' && this.item?.url) { + const path = this.item.url.replace(/^file:\/\//, '') + return window.location.origin + '/file?path=' + encodeURIComponent(path) + } + + return null + }, }, } @@ -280,6 +307,24 @@ export default { } } +.direct-url { + .right.side { + position: relative; + + button { + background: none; + border: none; + padding: 0; + margin-left: 1em; + + &:hover { + color: $default-hover-fg; + cursor: pointer; + } + } + } +} + .header { width: 100%; display: flex; diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Item.vue b/platypush/backend/http/webapp/src/components/panels/Media/Item.vue index 902f46caec..278956d2fd 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Item.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Item.vue @@ -5,18 +5,24 @@ @click.right.prevent="$refs.dropdown.toggle()" v-if="!hidden">
- +
-
+
+ v-if="(item.type === 'torrent' || item.type === 'youtube') && item.item_type !== 'channel' && item.item_type !== 'playlist'" /> + + + @@ -25,7 +31,7 @@
- + @@ -48,7 +54,17 @@ import Utils from "@/Utils"; export default { components: {Dropdown, DropdownItem, MediaImage}, mixins: [Utils], - emits: ['play', 'select', 'view', 'download'], + emits: [ + 'add-to-playlist', + 'download', + 'download-audio', + 'open-channel', + 'play', + 'remove-from-playlist', + 'select', + 'view', + ], + props: { item: { type: Object, @@ -64,6 +80,10 @@ export default { type: Boolean, default: false, }, + + playlist: { + type: String, + }, }, data() { @@ -81,7 +101,8 @@ export default { display: flex; flex-direction: column; align-items: center; - justify-content: space-between; + max-height: 23.5em; + height: 100%; cursor: initial !important; margin-bottom: 5px; border: 1px solid transparent; @@ -107,7 +128,6 @@ export default { display: flex; flex-direction: column; align-items: center; - flex: 1; .row { width: 100%; diff --git a/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue b/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue index 41eae208d0..204a719f3c 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue @@ -1,8 +1,8 @@ @@ -107,12 +135,8 @@ nav { list-style: none; padding: .6em; opacity: 0.7; - - &.selected, - &:hover { - border-radius: 1.2em; - margin: 0 0.2em; - } + border-radius: 1.2em; + margin: 0 0.2em; &:hover { background: $nav-entry-collapsed-hover-bg; @@ -122,6 +146,9 @@ nav { background: $nav-entry-collapsed-selected-bg; } + &.completed { + color: $ok-fg; + } } } diff --git a/platypush/backend/http/webapp/src/components/panels/Media/PlaylistAdder.vue b/platypush/backend/http/webapp/src/components/panels/Media/PlaylistAdder.vue new file mode 100644 index 0000000000..1bf1a25d24 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Media/PlaylistAdder.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue index 8de115bc2a..6979010108 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue @@ -2,13 +2,33 @@ import Utils from "@/Utils"; export default { - emits: ['back', 'path-change', 'play'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'back', + 'create-playlist', + 'download', + 'download-audio', + 'path-change', + 'play', + 'remove-from-playlist', + 'remove-playlist', + 'rename-playlist', + ], + props: { filter: { type: String, default: '', }, + + selectedPlaylist: { + default: null, + }, + + selectedChannel: { + default: null, + }, }, data() { diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/Nav.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/Nav.vue index b475082f91..96f760e13d 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/Nav.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/Nav.vue @@ -48,6 +48,9 @@ export default { @import "../style.scss"; .nav { + overflow-x: auto !important; + overflow-y: hidden !important; + .path .token .icon { margin-right: 0.5em; } diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue index c6491590d3..a64b85ab47 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue @@ -8,17 +8,36 @@
+ @add-to-playlist="$emit('add-to-playlist', $event)" + @download="$emit('download', $event)" + @download-audio="$emit('download-audio', $event)" + @open-channel="selectChannelFromItem" + @play="$emit('play', $event)" + v-if="selectedView === 'feed'" + /> + + v-else-if="selectedView === 'playlists'" + /> + + v-else-if="selectedView === 'subscriptions'" + /> +
@@ -52,8 +71,8 @@ export default { return { youtubeConfig: null, selectedView: null, - selectedPlaylist: null, - selectedChannel: null, + selectedPlaylist_: null, + selectedChannel_: null, path: [], } }, @@ -87,12 +106,36 @@ export default { } }, + async removeFromPlaylist(event) { + const playlistId = event.playlist_id + const videoId = event.item.url + this.loading = true + + try { + await this.request('youtube.remove_from_playlist', { + playlist_id: playlistId, + video_id: videoId, + }) + } finally { + this.loading = false + } + }, + + async createPlaylist(name) { + this.loading = true + try { + await this.request('youtube.create_playlist', {name: name}) + } finally { + this.loading = false + } + }, + selectView(view) { this.selectedView = view if (view === 'playlists') - this.selectedPlaylist = null + this.selectedPlaylist_ = null else if (view === 'subscriptions') - this.selectedChannel = null + this.selectedChannel_ = null if (view?.length) { this.path = [ @@ -107,22 +150,86 @@ export default { }, onPlaylistSelected(playlist) { - this.selectedPlaylist = playlist.id + this.selectedPlaylist_ = playlist + if (!playlist) + return + + this.selectedView = 'playlists' this.path.push({ title: playlist.name, }) }, onChannelSelected(channel) { - this.selectedChannel = channel.id + this.selectedChannel_ = channel + if (!channel) + return + + this.selectedView = 'subscriptions' this.path.push({ title: channel.name, }) }, + + initView() { + const args = this.getUrlArgs() + + if (args.section) + this.selectedView = args.section + + if (this.selectedView) + this.selectView(this.selectedView) + }, + + async selectChannelFromItem(item) { + if (!item.channel_url) + return + + const channel = await this.request( + 'youtube.get_channel', + {id: item.channel_url.split('/').pop()} + ) + + if (!channel) + return + + this.onChannelSelected(channel) + }, + }, + + watch: { + selectedPlaylist() { + this.onPlaylistSelected(this.selectedPlaylist) + }, + + selectedPlaylist_(value) { + if (value == null) + this.setUrlArgs({playlist: null}) + }, + + selectedChannel() { + this.onChannelSelected(this.selectedChannel) + }, + + selectedChannel_(value) { + if (value == null) + this.setUrlArgs({channel: null}) + }, + + selectedView() { + this.setUrlArgs({section: this.selectedView}) + }, }, mounted() { this.loadYoutubeConfig() + this.initView() + this.onPlaylistSelected(this.selectedPlaylist) + this.onChannelSelected(this.selectedChannel) + }, + + unmounted() { + this.setUrlArgs({section: null}) }, } @@ -140,6 +247,7 @@ export default { .body { height: calc(100% - $media-nav-height - 2px); margin-top: 2px; + overflow-y: auto; } } diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Channel.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Channel.vue index bebd83539c..1cbb4ad090 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Channel.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Channel.vue @@ -1,35 +1,59 @@ @@ -40,8 +64,15 @@ import Results from "@/components/panels/Media/Results"; import Utils from "@/Utils"; export default { - emits: ['play'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'download', + 'download-audio', + 'open-channel', + 'play', + ], + components: { Loading, Results, @@ -65,6 +96,7 @@ export default { loading: false, loadingNextPage: false, selectedResult: null, + subscribed: false, } }, @@ -81,107 +113,105 @@ export default { async loadChannel() { this.loading = true try { - this.channel = await this.request('youtube.get_channel', {id: this.id}) + await this.updateChannel(true) + this.subscribed = await this.request('youtube.is_subscribed', {channel_id: this.id}) } finally { this.loading = false } }, + async updateChannel(init) { + const channel = await this.request( + 'youtube.get_channel', + {id: this.id, next_page_token: this.channel?.next_page_token} + ) + + const itemsByUrl = this.itemsByUrl || {} + let items = channel.items + .filter(item => !itemsByUrl[item.url]) + .map(item => { + return { + type: 'youtube', + ...item, + } + }) + + if (!init) { + items = this.channel.items.concat(items) + } + + this.channel = channel + this.channel.items = items + }, + async loadNextPage() { - if (!this.channel?.next_page_token || this.loadingNextPage) + if (!this.channel?.next_page_token || this.loadingNextPage) { return + } + + this.loadingNextPage = true try { - const nextPage = await this.request( - 'youtube.get_channel', - {id: this.id, next_page_token: this.channel.next_page_token} - ) - - this.channel.items.push(...nextPage.items.filter(item => !this.itemsByUrl[item.url])) - this.channel.next_page_token = nextPage.next_page_token - this.$refs.results.maxResultIndex += this.$refs.results.resultIndexStep + await this.timeout(500) + await this.updateChannel() } finally { this.loadingNextPage = false } }, - onScroll(e) { - const el = e.target - if (!el) - return - - const bottom = (el.scrollHeight - el.scrollTop) <= el.clientHeight + 150 - if (!bottom) - return - - this.loadNextPage() + async toggleSubscription() { + const action = this.subscribed ? 'unsubscribe' : 'subscribe' + await this.request(`youtube.${action}`, {channel_id: this.id}) + this.subscribed = !this.subscribed }, }, - mounted() { - this.loadChannel() + async mounted() { + this.setUrlArgs({channel: this.id}) + await this.loadChannel() + }, + + unmounted() { + this.setUrlArgs({channel: null}) }, } diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue index 823e11145e..8c90ed9fda 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue @@ -1,6 +1,6 @@ @@ -90,12 +261,14 @@ export default { diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Subscriptions.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Subscriptions.vue index e97cdf7939..457ead8733 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Subscriptions.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Subscriptions.vue @@ -1,6 +1,6 @@ @@ -32,8 +39,15 @@ import Loading from "@/components/Loading"; import Utils from "@/Utils"; export default { - emits: ['play', 'select'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'download', + 'download-audio', + 'play', + 'select', + ], + components: { Channel, Loading, @@ -42,7 +56,7 @@ export default { props: { selectedChannel: { - type: String, + type: Object, default: null, }, @@ -79,10 +93,18 @@ export default { this.loading = false } }, + + initView() { + const args = this.getUrlArgs() + if (args.channel) { + this.$emit('select', {id: args.channel}) + } + }, }, - mounted() { - this.loadSubscriptions() + async mounted() { + await this.loadSubscriptions() + this.initView() }, } diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/header.scss b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/header.scss new file mode 100644 index 0000000000..b564d9af74 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/header.scss @@ -0,0 +1,128 @@ +$banner-height: var(--banner-height); +$info-bg: rgba(0, 0, 0, 0.5); +$info-fg: rgba(255, 255, 255, 0.9); + +@include until($tablet) { + .playlist-container, .channel { + --banner-height: 5em; + } +} + +@include from($tablet) { + .playlist-container, .channel { + --banner-height: 100px; + } +} + +.header { + border-bottom: $default-border-2; + margin-bottom: 0.5em; + position: relative; + overflow-y: auto; + + .banner { + height: $banner-height; + display: flex; + background-color: black; + justify-content: center; + + img { + max-width: 800px; + max-height: 100%; + flex: 1; + } + } + + .image { + height: 100px; + margin: -2.5em 2em 0.5em 0.5em; + + img { + height: 100%; + border-radius: 50%; + } + } + + .row { + display: flex; + + @include from($desktop) { + flex-direction: row; + } + + .info { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + background-color: $info-bg; + + .row { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex: 1; + padding: 0 0.5em; + } + } + } + + .info-container { + position: absolute; + top: 0; + width: 100%; + height: 100%; + background-color: $info-bg; + color: $info-fg; + display: flex; + flex-direction: column; + + a { + color: $info-fg !important; + + &:hover { + color: $default-hover-fg !important; + } + } + + .title { + letter-spacing: 0.1em; + color: $info-fg !important; + } + + .n-items { + /* Align to the right */ + margin-left: auto; + padding: 0 0.5em; + } + } + + .title-container { + max-height: var(--banner-height); + overflow-y: hidden; + } + + .title { + height: 100%; + align-content: center; + color: $default-fg-2; + font-size: 1.7em; + font-weight: bold; + margin: 0.5em 0; + text-decoration: dotted; + + &:hover { + color: $default-hover-fg; + } + } + + .description { + font-size: 0.9em; + margin-right: 0.5em; + } +} + +.media-results { + height: calc(100% - #{$banner-height} - 1em); +} diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Results.vue b/platypush/backend/http/webapp/src/components/panels/Media/Results.vue index b89684a8cd..6c602fd9e2 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Results.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Results.vue @@ -4,17 +4,24 @@
+ @download="$emit('download', item)" + @download-audio="$emit('download-audio', item)" + />
@@ -29,13 +36,28 @@ import Modal from "@/components/Modal"; export default { components: {Info, Item, Loading, Modal}, - emits: ['select', 'play', 'view', 'download', 'scroll-end'], + emits: [ + 'add-to-playlist', + 'download', + 'download-audio', + 'open-channel', + 'play', + 'remove-from-playlist', + 'scroll-end', + 'select', + 'view', + ], + props: { loading: { type: Boolean, default: false, }, + pluginName: { + type: String, + }, + results: { type: Array, default: () => [], @@ -59,6 +81,10 @@ export default { type: Number, default: 25, }, + + playlist: { + default: null, + }, }, data() { @@ -69,14 +95,18 @@ export default { computed: { visibleResults() { - return this.results + let results = this.results .filter((item) => { - if (!this.filter) + if (!this.filter?.length) return true return item.title.toLowerCase().includes(this.filter.toLowerCase()) }) - .slice(0, this.maxResultIndex) + + if (this.maxResultIndex != null) + results = results.slice(0, this.maxResultIndex) + + return results }, }, @@ -91,12 +121,19 @@ export default { return this.$emit('scroll-end') - this.maxResultIndex += this.resultIndexStep + + if (this.resultIndexStep != null) + this.maxResultIndex += this.resultIndexStep }, }, mounted() { this.$watch('selectedResult', (value) => { + if (value?.item_type === 'playlist' || value?.item_type === 'channel') { + this.$emit('select', null) + return + } + if (value == null) this.$refs.infoModal?.close() else @@ -114,6 +151,7 @@ export default { width: 100%; height: 100%; background: $background-color; + position: relative; .grid { height: 100%; diff --git a/platypush/backend/http/webapp/src/components/panels/Music/Playlists.vue b/platypush/backend/http/webapp/src/components/panels/Music/Playlists.vue index 5c5126d905..3aa1ac5c35 100644 --- a/platypush/backend/http/webapp/src/components/panels/Music/Playlists.vue +++ b/platypush/backend/http/webapp/src/components/panels/Music/Playlists.vue @@ -16,6 +16,7 @@ @add-to-queue="$emit('load-tracks', {tracks: $event, play: false})" @add-to-queue-and-play="$emit('load-tracks', {tracks: $event, play: true})" @back="$emit('playlist-edit', null)" + @download="$emit('download', $event)" @info="$emit('info', $event)" @move="$emit('track-move', {...$event, playlist: editedPlaylist})" @play="$emit('load-tracks', {tracks: [$event], play: true})" @@ -104,6 +105,7 @@ export default { emits: [ 'add-to-playlist', + 'download', 'info', 'load', 'load-tracks', diff --git a/platypush/backend/http/webapp/src/router/index.js b/platypush/backend/http/webapp/src/router/index.js index 85538f4796..8ac91e24bc 100644 --- a/platypush/backend/http/webapp/src/router/index.js +++ b/platypush/backend/http/webapp/src/router/index.js @@ -1,45 +1,39 @@ import { createWebHistory, createRouter } from "vue-router"; -import Dashboard from "@/views/Dashboard.vue"; -import NotFound from "@/views/NotFound"; -import Login from "@/views/Login"; -import Register from "@/views/Register"; -import Panel from "@/views/Panel"; -import Plugin from "@/views/Plugin"; const routes = [ { path: "/", name: "Panel", - component: Panel, + component: () => import(/* webpackChunkName: "panel" */ "@/views/Panel"), }, { path: "/dashboard/:name", name: "Dashboard", - component: Dashboard, + component: () => import(/* webpackChunkName: "dashboard" */ "@/views/Dashboard"), }, { path: "/plugin/:plugin", name: "Plugin", - component: Plugin, + component: () => import(/* webpackChunkName: "plugin" */ "@/views/Plugin"), }, { path: "/login", name: "Login", - component: Login, + component: () => import(/* webpackChunkName: "login" */ "@/views/Login"), }, { path: "/register", name: "Register", - component: Register, + component: () => import(/* webpackChunkName: "register" */ "@/views/Register"), }, { path: "/:catchAll(.*)", - component: NotFound, + component: () => import(/* webpackChunkName: "notfound" */ "@/views/NotFound"), }, ]; diff --git a/platypush/backend/http/webapp/src/style/animations.scss b/platypush/backend/http/webapp/src/style/animations.scss index 3523a814da..4d2ef72b82 100644 --- a/platypush/backend/http/webapp/src/style/animations.scss +++ b/platypush/backend/http/webapp/src/style/animations.scss @@ -26,3 +26,30 @@ display: none; } } + +.glow { + animation-duration: 2s; + -webkit-animation-duration: 2s; + animation-fill-mode: both; + animation-name: glow; + -webkit-animation-name: glow; +} + +.loop { + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; +} + +@keyframes glow { + 0% {opacity: 1; box-shadow: 0 0 5px #fff;} + 10% {opacity: 0.9; box-shadow: 0 0 10px $active-glow-fg-1;} + 20% {opacity: 0.8; box-shadow: 0 0 20px $active-glow-fg-1;} + 30% {opacity: 0.7; box-shadow: 0 0 30px $active-glow-fg-1;} + 40% {opacity: 0.6; box-shadow: 0 0 40px $active-glow-fg-1;} + 50% {opacity: 0.5; box-shadow: 0 0 50px $active-glow-fg-1;} + 60% {opacity: 0.6; box-shadow: 0 0 40px $active-glow-fg-1;} + 70% {opacity: 0.7; box-shadow: 0 0 30px $active-glow-fg-1;} + 80% {opacity: 0.8; box-shadow: 0 0 20px $active-glow-fg-1;} + 90% {opacity: 0.9; box-shadow: 0 0 10px $active-glow-fg-1;} + 100% {opacity: 1; box-shadow: 0 0 5px #fff;} +} diff --git a/platypush/backend/http/webapp/src/style/themes/light.scss b/platypush/backend/http/webapp/src/style/themes/light.scss index 37db58665d..87121a48e1 100644 --- a/platypush/backend/http/webapp/src/style/themes/light.scss +++ b/platypush/backend/http/webapp/src/style/themes/light.scss @@ -87,6 +87,8 @@ $default-link-fg: #5f7869 !default; /// Active $active-glow-bg-1: #d4ffe3 !default; $active-glow-bg-2: #9cdfb0 !default; +$active-glow-fg-1: #32b646 !default; +$active-glow-fg-2: #5f7869 !default; /// Hover $default-hover-fg: #35b870 !default; diff --git a/platypush/backend/http/webapp/src/utils/Api.vue b/platypush/backend/http/webapp/src/utils/Api.vue index 12305fcee7..52ab708855 100644 --- a/platypush/backend/http/webapp/src/utils/Api.vue +++ b/platypush/backend/http/webapp/src/utils/Api.vue @@ -72,7 +72,11 @@ export default { action: action, args: args, }, timeout, showError); - } + }, + + timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + }, }, } diff --git a/platypush/backend/http/webapp/src/utils/DateTime.vue b/platypush/backend/http/webapp/src/utils/DateTime.vue index c6bb8f2716..36917f2e5d 100644 --- a/platypush/backend/http/webapp/src/utils/DateTime.vue +++ b/platypush/backend/http/webapp/src/utils/DateTime.vue @@ -3,13 +3,17 @@ export default { name: "DateTime", methods: { formatDate(date, year=false) { - if (typeof date === 'string') + if (typeof date === 'number') + date = new Date(date * 1000) + else if (typeof date === 'string') date = new Date(Date.parse(date)) return date.toDateString().substring(0, year ? 15 : 10) }, formatTime(date, seconds=true) { + if (typeof date === 'number') + date = new Date(date * 1000) if (typeof date === 'string') date = new Date(Date.parse(date)) @@ -17,6 +21,8 @@ export default { }, formatDateTime(date, year=false, seconds=true, skipTimeIfMidnight=false) { + if (typeof date === 'number') + date = new Date(date * 1000) if (typeof date === 'string') date = new Date(Date.parse(date)) diff --git a/platypush/backend/http/webapp/src/utils/Types.vue b/platypush/backend/http/webapp/src/utils/Types.vue index 809ae76de5..09b858a1c3 100644 --- a/platypush/backend/http/webapp/src/utils/Types.vue +++ b/platypush/backend/http/webapp/src/utils/Types.vue @@ -114,6 +114,10 @@ export default { return true }, + + round(value, decimals) { + return Number(Math.round(value+'e'+decimals)+'e-'+decimals); + }, }, } diff --git a/platypush/backend/http/webapp/src/utils/Url.vue b/platypush/backend/http/webapp/src/utils/Url.vue index 4a7e17d9e3..6bd00db4ac 100644 --- a/platypush/backend/http/webapp/src/utils/Url.vue +++ b/platypush/backend/http/webapp/src/utils/Url.vue @@ -16,7 +16,7 @@ export default { .reduce((acc, obj) => { const tokens = obj.split('=') if (tokens[0]?.length) - acc[tokens[0]] = tokens[1] + acc[tokens[0]] = decodeURIComponent(tokens[1]) return acc }, {}) }, @@ -40,10 +40,24 @@ export default { window.location.href = location }, + encodeValue(value) { + if (!value?.length || value === 'null' || value === 'undefined') + return '' + + // Don't re-encode the value if it's already encoded + if (value.match(/%[0-9A-F]{2}/i)) + return value + + return encodeURIComponent(value) + }, + fragmentFromArgs(args) { return Object.entries(args) + .filter( + ([key, value]) => this.encodeValue(key)?.length && this.encodeValue(value)?.length + ) .map( - ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}` + ([key, value]) => `${this.encodeValue(key)}=${this.encodeValue(value)}` ) .join('&') },