Add notifications for new blog posts and app updates

This commit is contained in:
Preston 2020-09-20 14:22:39 -04:00
parent c36a9d9254
commit b0d1ddf1ac
16 changed files with 324 additions and 31 deletions

21
package-lock.json generated
View File

@ -3186,8 +3186,7 @@
"abbrev": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
"dev": true
}, },
"accepts": { "accepts": {
"version": "1.3.7", "version": "1.3.7",
@ -13583,6 +13582,24 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"markdown": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/markdown/-/markdown-0.5.0.tgz",
"integrity": "sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=",
"requires": {
"nopt": "~2.1.1"
},
"dependencies": {
"nopt": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz",
"integrity": "sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=",
"requires": {
"abbrev": "1"
}
}
}
},
"matcher": { "matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",

View File

@ -21,6 +21,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.uniqwith": "^4.5.0", "lodash.uniqwith": "^4.5.0",
"markdown": "^0.5.0",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"mediaelement": "^4.2.16", "mediaelement": "^4.2.16",
"nedb": "^1.8.0", "nedb": "^1.8.0",

View File

@ -24,12 +24,16 @@ body {
} }
.banner { .banner {
margin-top: 70px; width: 85%;
margin-bottom: -65px; }
.flexBox {
margin-top: 60px;
margin-bottom: -75px;
} }
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
transition: opacity .2s; transition: opacity .15s;
} }
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0; opacity: 0;
@ -42,7 +46,11 @@ body {
} }
.banner { .banner {
margin-top: 70px; width: 90%;
margin-bottom: -65px; }
.flexBox {
margin-top: 60px;
margin-bottom: -75px;
} }
} }

View File

@ -1,10 +1,16 @@
import Vue from 'vue' import Vue from 'vue'
import { ObserveVisibility } from 'vue-observe-visibility' import { ObserveVisibility } from 'vue-observe-visibility'
import FtFlexBox from './components/ft-flex-box/ft-flex-box.vue'
import TopNav from './components/top-nav/top-nav.vue' import TopNav from './components/top-nav/top-nav.vue'
import SideNav from './components/side-nav/side-nav.vue' import SideNav from './components/side-nav/side-nav.vue'
import FtNotificationBanner from './components/ft-notification-banner/ft-notification-banner.vue'
import FtPrompt from './components/ft-prompt/ft-prompt.vue'
import FtButton from './components/ft-button/ft-button.vue'
import FtToast from './components/ft-toast/ft-toast.vue' import FtToast from './components/ft-toast/ft-toast.vue'
import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue' import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue'
import $ from 'jquery' import $ from 'jquery'
import { markdown } from 'markdown'
import Parser from 'rss-parser'
let useElectron let useElectron
let shell let shell
@ -22,14 +28,26 @@ if (window && window.process && window.process.type === 'renderer') {
export default Vue.extend({ export default Vue.extend({
name: 'App', name: 'App',
components: { components: {
FtFlexBox,
TopNav, TopNav,
SideNav, SideNav,
FtNotificationBanner,
FtPrompt,
FtButton,
FtToast, FtToast,
FtProgressBar FtProgressBar
}, },
data: function () { data: function () {
return { return {
hideOutlines: true hideOutlines: true,
showUpdatesBanner: false,
showBlogBanner: false,
showReleaseNotes: false,
updateBannerMessage: '',
blogBannerMessage: '',
latestBlogUrl: '',
updateChangelog: '',
changeLogTitle: ''
} }
}, },
computed: { computed: {
@ -41,6 +59,12 @@ export default Vue.extend({
}, },
isRightAligned: function () { isRightAligned: function () {
return this.$i18n.locale === 'ar' return this.$i18n.locale === 'ar'
},
checkForUpdates: function () {
return this.$store.getters.getCheckForUpdates
},
checkForBlogPosts: function () {
return this.$store.getters.getCheckForBlogPosts
} }
}, },
mounted: function () { mounted: function () {
@ -56,6 +80,11 @@ export default Vue.extend({
this.activateKeyboardShortcuts() this.activateKeyboardShortcuts()
this.openAllLinksExternally() this.openAllLinksExternally()
} }
setTimeout(() => {
this.checkForNewUpdates()
this.checkForNewBlogPosts()
}, 500)
}, },
methods: { methods: {
checkLocale: function () { checkLocale: function () {
@ -105,6 +134,80 @@ export default Vue.extend({
localStorage.setItem('secColor', theme.secColor) localStorage.setItem('secColor', theme.secColor)
}, },
checkForNewUpdates: function () {
if (this.checkForUpdates) {
const { version } = require('../../package.json')
const requestUrl = 'https://api.github.com/repos/freetubeapp/freetube-vue/releases'
$.getJSON(requestUrl, (response) => {
const tagName = response[0].tag_name
const versionNumber = tagName.replace('v', '').replace('-beta', '')
this.updateChangelog = markdown.toHTML(response[0].body)
this.changeLogTitle = response[0].name
const message = this.$t('Version $ is now available! Click for more details')
this.updateBannerMessage = message.replace('$', versionNumber)
if (version < versionNumber) {
this.showUpdatesBanner = true
}
}).fail((xhr, textStatus, error) => {
console.log(xhr)
console.log(textStatus)
console.log(requestUrl)
console.log(error)
})
}
},
checkForNewBlogPosts: function () {
if (this.checkForBlogPosts) {
const parser = new Parser()
const feedUrl = 'https://write.as/freetube/feed/'
let lastAppWasRunning = localStorage.getItem('lastAppWasRunning')
if (lastAppWasRunning !== null) {
lastAppWasRunning = new Date(lastAppWasRunning)
}
parser.parseURL(feedUrl).then((response) => {
const latestBlog = response.items[0]
const latestPubDate = new Date(latestBlog.pubDate)
if (lastAppWasRunning === null || latestPubDate > lastAppWasRunning) {
const message = this.$t('A new blog is now available, $. Click to view more')
this.blogBannerMessage = message.replace('$', latestBlog.title)
this.latestBlogUrl = latestBlog.link
this.showBlogBanner = true
}
localStorage.setItem('lastAppWasRunning', new Date())
})
}
},
handleUpdateBannerClick: function (response) {
if (response !== false) {
this.showReleaseNotes = true
} else {
this.showUpdatesBanner = false
}
},
handleNewBlogBannerClick: function (response) {
if (response) {
shell.openExternal(this.latestBlogUrl)
}
this.showBlogBanner = false
},
openDownloadsPage: function () {
const url = 'https://freetubeapp.io#download'
shell.openExternal(url)
this.showReleaseNotes = false
this.showUpdatesBanner = false
},
activateKeyboardShortcuts: function () { activateKeyboardShortcuts: function () {
$(document).on('keydown', this.handleKeyboardShortcuts) $(document).on('keydown', this.handleKeyboardShortcuts)
$(document).on('mousedown', () => { $(document).on('mousedown', () => {

View File

@ -8,6 +8,24 @@
> >
<top-nav ref="topNav" /> <top-nav ref="topNav" />
<side-nav ref="sideNav" /> <side-nav ref="sideNav" />
<ft-flex-box
v-if="showUpdatesBanner || showBlogBanner"
class="flexBox routerView"
:class="{ expand: !isOpen }"
>
<ft-notification-banner
v-if="showUpdatesBanner"
class="banner"
:message="updateBannerMessage"
@click="handleUpdateBannerClick"
/>
<ft-notification-banner
v-if="showBlogBanner"
class="banner"
:message="blogBannerMessage"
@click="handleNewBlogBannerClick"
/>
</ft-flex-box>
<transition <transition
mode="out-in" mode="out-in"
name="fade" name="fade"
@ -20,6 +38,21 @@
/> />
<!-- </keep-alive> --> <!-- </keep-alive> -->
</transition> </transition>
<ft-prompt
v-if="showReleaseNotes"
@click="showReleaseNotes = !showReleaseNotes"
>
<h2>
{{ changeLogTitle }}
</h2>
<span v-html="updateChangelog" />
<ft-flex-box>
<ft-button
:label="$t('Download From Site')"
@click="openDownloadsPage"
/>
</ft-flex-box>
</ft-prompt>
<ft-toast /> <ft-toast />
<ft-progress-bar <ft-progress-bar
v-if="showProgressBar" v-if="showProgressBar"

View File

@ -0,0 +1,27 @@
.ftNotificationBanner {
background-color: var(--primary-color);
color: var(--text-with-main-color);
/*
background-color: var(--accent-color);
color: var(--text-with-accent-color);
*/
margin: 4px;
padding: 16px;
padding-top: 3px;
padding-bottom: 5px;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
position: relative;
cursor: pointer;
}
.message {
margin-right: 25px;
cursor: pointer;
}
.bannerIcon {
position: absolute;
top: 35%;
right: 10px;
cursor: pointer;
}

View File

@ -0,0 +1,26 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtNotificationBanner',
props: {
message: {
type: String,
required: true
}
},
computed: {
progressBarPercentage: function () {
return this.$store.getters.getProgressBarPercentage
}
},
methods: {
handleClick: function (response) {
this.$emit('click', response)
},
handleClose: function (event) {
event.stopPropagation()
this.handleClick(false)
}
}
})

View File

@ -0,0 +1,24 @@
<template>
<div
class="ftNotificationBanner"
@click="handleClick(true)"
>
<div
class="message"
>
<slot>
<p>
{{ message }}
</p>
</slot>
</div>
<font-awesome-icon
class="bannerIcon"
icon="times"
@click="handleClose"
/>
</div>
</template>
<script src="./ft-notification-banner.js" />
<style scoped src="./ft-notification-banner.css" />

View File

@ -11,7 +11,11 @@
.promptCard { .promptCard {
width: 95%; width: 95%;
margin-top: 40vh; margin: 0;
position: absolute;
top: 40%;
-ms-transform: translateY(-40%);
transform: translateY(-40%);
} }
.center { .center {

View File

@ -547,6 +547,9 @@ export default Vue.extend({
checkForUpdates: function () { checkForUpdates: function () {
return this.$store.getters.getCheckForUpdates return this.$store.getters.getCheckForUpdates
}, },
checkForBlogPosts: function () {
return this.$store.getters.getCheckForBlogPosts
},
backendPreference: function () { backendPreference: function () {
return this.$store.getters.getBackendPreference return this.$store.getters.getBackendPreference
}, },
@ -658,6 +661,7 @@ export default Vue.extend({
'updateEnableSearchSuggestions', 'updateEnableSearchSuggestions',
'updateBackendFallback', 'updateBackendFallback',
'updateCheckForUpdates', 'updateCheckForUpdates',
'updateCheckForBlogPosts',
'updateBarColor', 'updateBarColor',
'updateBackendPreference', 'updateBackendPreference',
'updateLandingPage', 'updateLandingPage',

View File

@ -7,24 +7,36 @@
> >
{{ $t("Settings.General Settings.General Settings") }} {{ $t("Settings.General Settings.General Settings") }}
</h3> </h3>
<ft-flex-box class="generalSettingsFlexBox"> <div class="switchColumnGrid">
<ft-toggle-switch <div class="switchColumn">
:label="$t('Settings.General Settings.Fallback to Non-Preferred Backend on Failure')" <ft-toggle-switch
:default-value="backendFallback" :label="$t('Settings.General Settings.Check for Updates')"
@change="updateBackendFallback" :default-value="checkForUpdates"
/> :compact="true"
<ft-toggle-switch @change="updateCheckForUpdates"
:label="$t('Settings.General Settings.Enable Search Suggestions')" />
:default-value="enableSearchSuggestions" <ft-toggle-switch
@change="updateEnableSearchSuggestions" :label="$t('Settings.General Settings.Fallback to Non-Preferred Backend on Failure')"
/> :default-value="backendFallback"
<ft-toggle-switch :compact="true"
v-if="false" @change="updateBackendFallback"
:label="$t('Settings.General Settings.Check for Updates')" />
:default-value="checkForUpdates" </div>
@change="updateCheckForUpdates" <div class="switchColumn">
/> <ft-toggle-switch
</ft-flex-box> :label="$t('Settings.General Settings.Check for Latest Blog Posts')"
:default-value="checkForBlogPosts"
:compact="true"
@change="updateCheckForBlogPosts"
/>
<ft-toggle-switch
:label="$t('Settings.General Settings.Enable Search Suggestions')"
:default-value="enableSearchSuggestions"
:compact="true"
@change="updateEnableSearchSuggestions"
/>
</div>
</div>
<div class="switchGrid"> <div class="switchGrid">
<ft-select <ft-select
:placeholder="$t('Settings.General Settings.Preferred API Backend.Preferred API Backend')" :placeholder="$t('Settings.General Settings.Preferred API Backend.Preferred API Backend')"

View File

@ -1,6 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue' import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue'
// import { shell } from 'electron' import { shell } from 'electron'
export default Vue.extend({ export default Vue.extend({
name: 'FtElementList', name: 'FtElementList',
@ -101,13 +101,13 @@ export default Vue.extend({
navigator.clipboard.writeText(youtubeUrl) navigator.clipboard.writeText(youtubeUrl)
break break
case 'openYoutube': case 'openYoutube':
// shell.openExternal(youtubeUrl) shell.openExternal(youtubeUrl)
break break
case 'copyInvidious': case 'copyInvidious':
navigator.clipboard.writeText(invidiousUrl) navigator.clipboard.writeText(invidiousUrl)
break break
case 'openInvidious': case 'openInvidious':
// shell.openExternal(invidiousUrl) shell.openExternal(invidiousUrl)
break break
} }
} }

View File

@ -70,7 +70,7 @@ export default Vue.extend({
} }
}) })
this.debounceSearchResults = debounce(this.getSearchSuggestions, 500) this.debounceSearchResults = debounce(this.getSearchSuggestions, 200)
}, },
methods: { methods: {
goToSearch: function (query) { goToSearch: function (query) {

View File

@ -30,6 +30,7 @@ const state = {
currentTheme: 'lightRed', currentTheme: 'lightRed',
backendFallback: true, backendFallback: true,
checkForUpdates: true, checkForUpdates: true,
checkForBlogPosts: true,
backendPreference: 'local', backendPreference: 'local',
landingPage: 'subscriptions', landingPage: 'subscriptions',
region: 'US', region: 'US',
@ -70,6 +71,10 @@ const getters = {
return state.checkForUpdates return state.checkForUpdates
}, },
getCheckForBlogPosts: () => {
return state.checkForBlogPosts
},
getBarColor: () => { getBarColor: () => {
return state.barColor return state.barColor
}, },
@ -194,6 +199,9 @@ const actions = {
case 'checkForUpdates': case 'checkForUpdates':
commit('setCheckForUpdates', result.value) commit('setCheckForUpdates', result.value)
break break
case 'checkForBlogPosts':
commit('setCheckForBlogPosts', result.value)
break
case 'enableSearchSuggestions': case 'enableSearchSuggestions':
commit('setEnableSearchSuggestions', result.value) commit('setEnableSearchSuggestions', result.value)
break break
@ -300,6 +308,14 @@ const actions = {
}) })
}, },
updateCheckForBlogPosts ({ commit }, checkForBlogPosts) {
settingsDb.update({ _id: 'checkForBlogPosts' }, { _id: 'checkForBlogPosts', value: checkForBlogPosts }, { upsert: true }, (err, numReplaced) => {
if (!err) {
commit('setCheckForBlogPosts', checkForBlogPosts)
}
})
},
updateEnableSearchSuggestions ({ commit }, enableSearchSuggestions) { updateEnableSearchSuggestions ({ commit }, enableSearchSuggestions) {
settingsDb.update({ _id: 'enableSearchSuggestions' }, { _id: 'enableSearchSuggestions', value: enableSearchSuggestions }, { upsert: true }, (err, numReplaced) => { settingsDb.update({ _id: 'enableSearchSuggestions' }, { _id: 'enableSearchSuggestions', value: enableSearchSuggestions }, { upsert: true }, (err, numReplaced) => {
if (!err) { if (!err) {
@ -502,6 +518,9 @@ const mutations = {
setCheckForUpdates (state, checkForUpdates) { setCheckForUpdates (state, checkForUpdates) {
state.checkForUpdates = checkForUpdates state.checkForUpdates = checkForUpdates
}, },
setCheckForBlogPosts (state, checkForBlogPosts) {
state.checkForBlogPosts = checkForBlogPosts
},
setBackendPreference (state, backendPreference) { setBackendPreference (state, backendPreference) {
state.backendPreference = backendPreference state.backendPreference = backendPreference
}, },

View File

@ -7,6 +7,7 @@ const state = {
trendingCache: null, trendingCache: null,
showProgressBar: false, showProgressBar: false,
progressBarPercentage: 0, progressBarPercentage: 0,
recentBlogPosts: [],
searchSettings: { searchSettings: {
sortBy: 'relevance', sortBy: 'relevance',
time: '', time: '',
@ -86,6 +87,10 @@ const getters = {
getProgressBarPercentage () { getProgressBarPercentage () {
return state.progressBarPercentage return state.progressBarPercentage
},
getRecentBlogPosts () {
return state.recentBlogPosts
} }
} }
@ -399,6 +404,10 @@ const mutations = {
setSearchDuration (state, value) { setSearchDuration (state, value) {
state.searchSettings.duration = value state.searchSettings.duration = value
},
setRecentBlogPosts (state, value) {
state.recentBlogPosts = value
} }
} }

View File

@ -29,6 +29,10 @@ Close: Close
Back: Back Back: Back
Forward: Forward Forward: Forward
Version $ is now available! Click for more details: Version $ is now available! Click for more details
Download From Site: Download From Site
A new blog is now available, $. Click to view more: A new blog is now available, $. Click to view more
# Search Bar # Search Bar
Search / Go to URL: Search / Go to URL Search / Go to URL: Search / Go to URL
# In Filter Button # In Filter Button
@ -87,6 +91,8 @@ Settings:
Settings: Settings Settings: Settings
General Settings: General Settings:
General Settings: General Settings General Settings: General Settings
Check for Updates: Check for Updates
Check for Latest Blog Posts: Check for Latest Blog Posts
Fallback to Non-Preferred Backend on Failure: Fallback to Non-Preferred Backend Fallback to Non-Preferred Backend on Failure: Fallback to Non-Preferred Backend
on Failure on Failure
Enable Search Suggestions: Enable Search Suggestions Enable Search Suggestions: Enable Search Suggestions