Finish Profile Logic and working subscriptions

This commit is contained in:
Preston 2020-08-31 17:35:22 -04:00
parent 0680e6c5b6
commit 1e035105d1
30 changed files with 957 additions and 88 deletions

View File

@ -3,6 +3,7 @@ import { ObserveVisibility } from 'vue-observe-visibility'
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 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 $ from 'jquery' import $ from 'jquery'
let useElectron let useElectron
@ -23,11 +24,15 @@ export default Vue.extend({
components: { components: {
TopNav, TopNav,
SideNav, SideNav,
FtToast FtToast,
FtProgressBar
}, },
computed: { computed: {
isOpen: function () { isOpen: function () {
return this.$store.getters.getIsSideNavOpen return this.$store.getters.getIsSideNavOpen
},
showProgressBar: function () {
return this.$store.getters.getShowProgressBar
} }
}, },
mounted: function () { mounted: function () {

View File

@ -15,6 +15,9 @@
<!-- </keep-alive> --> <!-- </keep-alive> -->
</Transition> </Transition>
<ft-toast /> <ft-toast />
<ft-progress-bar
v-if="showProgressBar"
/>
</div> </div>
</template> </template>

View File

@ -0,0 +1,65 @@
.colorOption {
width: 50px;
height: 50px;
margin: 10px;
cursor: pointer;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.initial {
font-size: 25px;
text-align: center;
position: relative;
bottom: 27px;
}
#profileList {
display: none;
position: absolute;
top: 60px;
right: 10px;
min-width: 250px;
height: 300px;
padding: 5px;
background-color: var(--card-bg-color);
box-shadow: 0 1px 2px rgba(0,0,0,.1);
}
#profileList:focus {
display: inline;
outline: none;
}
.profileWrapper {
margin-top: 60px;
height: 240px;
overflow-y: auto;
}
.profile {
cursor: pointer;
}
.profile:hover {
background-color: var(--side-nav-hover-color);
}
.profile .colorOption {
float: left;
position: relative;
bottom: 5px;
}
.profileListTitle {
position: absolute;
top: -15px;
left: 10px;
}
.profileSettings {
float: right;
position: absolute;
top: 10px;
right: 5px;
}

View File

@ -0,0 +1,79 @@
import Vue from 'vue'
import { mapActions } from 'vuex'
import $ from 'jquery'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
export default Vue.extend({
name: 'FtProfileSelector',
components: {
'ft-card': FtCard,
'ft-icon-button': FtIconButton
},
data: function () {
return {
showProfileList: false
}
},
computed: {
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
defaultProfile: function () {
return this.$store.getters.getDefaultProfile
},
activeProfileInitial: function () {
return this.activeProfile.name.slice(0, 1).toUpperCase()
},
profileInitials: function () {
return this.profileList.map((profile) => {
return profile.name.slice(0, 1).toUpperCase()
})
}
},
mounted: function () {
$('#profileList').focusout(() => {
$('#profileList')[0].style.display = 'none'
})
},
methods: {
toggleProfileList: function () {
$('#profileList')[0].style.display = 'inline'
$('#profileList').focus()
},
openProfileSettings: function () {
this.$router.push({
path: '/settings/profile/'
})
$('#profileList').focusout()
},
setActiveProfile: function (profile) {
if (this.profileList[this.activeProfile]._id === profile._id) {
return
}
const index = this.profileList.findIndex((x) => {
return x._id === profile._id
})
if (index === -1) {
return
}
this.updateActiveProfile(index)
this.showToast({
message: `${profile.name} is now the active profile`
})
$('#profileList').focusout()
},
...mapActions([
'showToast',
'updateActiveProfile'
])
}
})

View File

@ -0,0 +1,57 @@
<template>
<div>
<div
class="colorOption"
:style="{ background: profileList[activeProfile].bgColor, color: profileList[activeProfile].textColor }"
@click="toggleProfileList"
>
<p
class="initial"
>
{{ profileInitials[activeProfile] }}
</p>
</div>
<ft-card
id="profileList"
tabindex="-1"
>
<h3
class="profileListTitle"
>
Profile Select
</h3>
<ft-icon-button
class="profileSettings"
icon="sliders-h"
@click="openProfileSettings"
/>
<div
class="profileWrapper"
>
<div
class="profile"
v-for="(profile, index) in profileList"
:key="index"
@click="setActiveProfile(profile)"
>
<div
class="colorOption"
:style="{ background: profile.bgColor, color: profile.textColor }"
>
<p
class="initial"
>
{{ profileInitials[index] }}
</p>
</div>
<p>
{{ profile.name }}
</p>
</div>
</div>
</ft-card>
</div>
</template>
<script src="./ft-profile-selector.js" />
<style scoped src="./ft-profile-selector.css" />

View File

@ -0,0 +1,9 @@
.progressBar {
position: fixed;
height: 3px;
bottom: 0px;
left: 0px;
background-color: var(--primary-color);
z-index: 1;
transition: width 0.5s;
}

View File

@ -0,0 +1,10 @@
import Vue from 'vue'
export default Vue.extend({
name: 'FtProgressBar',
computed: {
progressBarPercentage: function () {
return this.$store.getters.getProgressBarPercentage
}
}
})

View File

@ -0,0 +1,9 @@
<template>
<div
class="progressBar"
:style="{ width: progressBarPercentage + '%' }"
/>
</template>
<script src="./ft-progress-bar.js" />
<style scoped src="./ft-progress-bar.css" />

View File

@ -30,7 +30,7 @@
margin-top: 10px; margin-top: 10px;
} }
.navOption { .navOption, .navChannel {
position: relative; position: relative;
padding: 5px; padding: 5px;
cursor: pointer; cursor: pointer;
@ -40,14 +40,14 @@
display: none; display: none;
} }
.navOption:hover { .navOption:hover, .navChannel:hover {
background-color: var(--side-nav-hover-color); background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in; -moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in; -o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in; transition: background 0.2s ease-in;
} }
.navOption:active { .navOption:active, .navChannel:active {
background-color: var(--side-nav-active-color); background-color: var(--side-nav-active-color);
-moz-transition: background 0.2s ease-in; -moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in; -o-transition: background 0.2s ease-in;
@ -63,6 +63,26 @@
display: inline-block; display: inline-block;
} }
.navChannel .navLabel {
font-size: 11px;
width: 150px;
margin-left: 40px;
}
.thumbnailContainer {
width: 35px;
margin: 0;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
.channelThumbnail {
border-radius: 50%;
width: 35px;
}
.closed { .closed {
width: 80px; width: 80px;
} }
@ -107,6 +127,24 @@
font-size: 11px; font-size: 11px;
} }
.closed .navChannel {
width: 100%;
padding: 0px;
padding-top: 10px;
padding-bottom: 10px;
}
.closed .thumbnailContainer {
position: static;
display: block;
float: none;
margin-left: 0px;
margin: 0 auto;
top: 0px;
-ms-transform: none;
transform: none;
}
@media only screen and (max-width: 680px) { @media only screen and (max-width: 680px) {
.inner { .inner {
display: contents; /* sunglasses emoji */ display: contents; /* sunglasses emoji */

View File

@ -12,11 +12,35 @@ export default Vue.extend({
computed: { computed: {
isOpen: function () { isOpen: function () {
return this.$store.getters.getIsSideNavOpen return this.$store.getters.getIsSideNavOpen
},
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeSubscriptions: function () {
const profile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
return profile.subscriptions.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) {
return -1
}
if (nameA > nameB) {
return 1
}
return 0
})
} }
}, },
methods: { methods: {
navigate: function (route) { navigate: function (route) {
router.push('/' + route) router.push('/' + route)
},
goToChannel: function (id) {
this.$router.push({ path: `/channel/${id}` })
} }
} }
}) })

View File

@ -16,10 +16,6 @@
<p class="navLabel"> <p class="navLabel">
{{ $t("Subscriptions.Subscriptions") }} {{ $t("Subscriptions.Subscriptions") }}
</p> </p>
<font-awesome-icon
class="refreshIcon"
icon="sync"
/>
</div> </div>
<div <div
class="navOption mobileHidden" class="navOption mobileHidden"
@ -87,7 +83,7 @@
</div> </div>
<div <div
class="navOption mobileHidden" class="navOption mobileHidden"
@click="navigate('settings/profile')" @click="navigate('about')"
> >
<font-awesome-icon <font-awesome-icon
icon="info-circle" icon="info-circle"
@ -98,6 +94,28 @@
</p> </p>
</div> </div>
<hr> <hr>
<div
v-for="(channel, index) in activeSubscriptions"
:key="index"
class="navChannel mobileHidden"
:title="channel.name"
@click="goToChannel(channel.id)"
>
<div
class="thumbnailContainer"
>
<img
class="channelThumbnail"
:src="channel.thumbnail"
/>
</div>
<p
class="navLabel"
v-if="isOpen"
>
{{ channel.name }}
</p>
</div>
</div> </div>
</ft-flex-box> </ft-flex-box>
</template> </template>

View File

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import FtInput from '../ft-input/ft-input.vue' import FtInput from '../ft-input/ft-input.vue'
import FtSearchFilters from '../ft-search-filters/ft-search-filters.vue' import FtSearchFilters from '../ft-search-filters/ft-search-filters.vue'
import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue'
import $ from 'jquery' import $ from 'jquery'
import router from '../../router/index.js' import router from '../../router/index.js'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
@ -10,7 +11,8 @@ export default Vue.extend({
name: 'TopNav', name: 'TopNav',
components: { components: {
FtInput, FtInput,
FtSearchFilters FtSearchFilters,
FtProfileSelector
}, },
data: () => { data: () => {
return { return {

View File

@ -59,6 +59,9 @@
display: flex display: flex
align-items: center align-items: center
&.profiles
justify-content: flex-end
.navSearchIcon .navSearchIcon
@media only screen and (min-width: 681px) @media only screen and (min-width: 681px)
display: none display: none

View File

@ -55,7 +55,7 @@
:class="{ expand: !isSideNavOpen }" :class="{ expand: !isSideNavOpen }"
/> />
</div> </div>
<div class="side" /> <ft-profile-selector class="side profiles" />
</div> </div>
</template> </template>

View File

@ -79,6 +79,14 @@ export default Vue.extend({
return this.$store.getters.getUsingElectron return this.$store.getters.getUsingElectron
}, },
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
formatTypeNames: function () { formatTypeNames: function () {
return [ return [
this.$t('Change Format.Use Dash Formats').toUpperCase(), this.$t('Change Format.Use Dash Formats').toUpperCase(),
@ -99,8 +107,24 @@ export default Vue.extend({
return this.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ` ${this.$t('Video.Views').toLowerCase()}` return this.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ` ${this.$t('Video.Views').toLowerCase()}`
}, },
isSubscribed: function () {
const subIndex = this.profileList[this.activeProfile].subscriptions.findIndex((channel) => {
return channel.id === this.channelId
})
if (subIndex === -1) {
return false
} else {
return true
}
},
subscribedText: function () { subscribedText: function () {
return `${this.$t('Channel.Subscribe').toUpperCase()} ${this.subscriptionCountText}` if (this.isSubscribed) {
return `${this.$t('Channel.Unsubscribe').toUpperCase()} ${this.subscriptionCountText}`
} else {
return `${this.$t('Channel.Subscribe').toUpperCase()} ${this.subscriptionCountText}`
}
}, },
dateString() { dateString() {
@ -116,9 +140,74 @@ export default Vue.extend({
}, },
handleSubscription: function () { handleSubscription: function () {
this.showToast({ const currentProfile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
message: 'Subscriptions have not yet been implemented' const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
})
if (this.isSubscribed) {
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
return channel.id !== this.channelId
})
this.updateProfile(currentProfile)
this.showToast({
message: 'Channel has been removed from your subscriptions'
})
if (this.activeProfile === 0) {
// Check if a subscription exists in a different profile.
// Remove from there as well.
let duplicateSubscriptions = 0
this.profileList.forEach((profile) => {
if (profile._id === 'allChannels') {
return
}
const parsedProfile = JSON.parse(JSON.stringify(profile))
const index = parsedProfile.subscriptions.findIndex((channel) => {
return channel.id === this.channelId
})
if (index !== -1) {
duplicateSubscriptions++
parsedProfile.subscriptions = parsedProfile.subscriptions.filter((x) => {
return x.id !== this.channelId
})
this.updateProfile(parsedProfile)
}
})
if (duplicateSubscriptions > 0) {
this.showToast({
message: `Removed subscription from ${duplicateSubscriptions} other channel(s)`
})
}
}
} else {
const subscription = {
id: this.channelId,
name: this.channelName,
thumbnail: this.channelThumbnail
}
currentProfile.subscriptions.push(subscription)
this.updateProfile(currentProfile)
this.showToast({
message: 'Added channel to your subscriptions'
})
if (this.activeProfile !== 0) {
const index = primaryProfile.subscriptions.findIndex((channel) => {
return channel.id === this.channelId
})
if (index === -1) {
primaryProfile.subscriptions.push(subscription)
this.updateProfile(primaryProfile)
}
}
}
}, },
handleFormatChange: function (format) { handleFormatChange: function (format) {
@ -136,7 +225,8 @@ export default Vue.extend({
}, },
...mapActions([ ...mapActions([
'showToast' 'showToast',
'updateProfile'
]) ])
} }
}) })

View File

@ -25,13 +25,23 @@ const profileDb = new Datastore({
}) })
const state = { const state = {
profileList: [], profileList: [{
activeProfile: 'allChannels' _id: 'allChannels',
name: 'All Channels',
bgColor: '#000000',
textColor: '#FFFFFF',
subscriptions: []
}],
activeProfile: 0
} }
const getters = { const getters = {
getProfileList: () => { getProfileList: () => {
return state.profileList return state.profileList
},
getActiveProfile: () => {
return state.activeProfile
} }
} }
@ -39,11 +49,23 @@ const actions = {
grabAllProfiles ({ dispatch, commit }, defaultName = null) { grabAllProfiles ({ dispatch, commit }, defaultName = null) {
profileDb.find({}, (err, results) => { profileDb.find({}, (err, results) => {
if (!err) { if (!err) {
console.log(results)
if (results.length === 0) { if (results.length === 0) {
dispatch('createDefaultProfile', defaultName) dispatch('createDefaultProfile', defaultName)
} else { } else {
commit('setProfileList', results) // We want the primary profile to always be first
// So sort with that then sort alphabetically by profile name
const profiles = results.sort((a, b) => {
if (a._id === 'allChannels') {
return -1
}
if (b._id === 'allChannels') {
return 1
}
return b.name - a.name
})
commit('setProfileList', profiles)
} }
} }
}) })
@ -94,18 +116,25 @@ const actions = {
}) })
}, },
removeProfile ({ dispatch }, videoId) { removeProfile ({ dispatch }, profileId) {
profileDb.remove({ videoId: videoId }, (err, numReplaced) => { profileDb.remove({ _id: profileId }, (err, numReplaced) => {
if (!err) { if (!err) {
dispatch('grabHistory') dispatch('grabAllProfiles')
} }
}) })
},
updateActiveProfile ({ commit }, index) {
commit('setActiveProfile', index)
} }
} }
const mutations = { const mutations = {
setProfileList (state, profileList) { setProfileList (state, profileList) {
state.profileList = profileList state.profileList = profileList
},
setActiveProfile (state, activeProfile) {
state.activeProfile = activeProfile
} }
} }

View File

@ -1,61 +1,40 @@
import Datastore from 'nedb' import ytch from 'yt-channel-info'
let dbLocation
if (window && window.process && window.process.type === 'renderer') {
// Electron is being used
let dbLocation = localStorage.getItem('dbLocation')
if (dbLocation === null) {
const electron = require('electron')
dbLocation = electron.remote.app.getPath('userData')
}
dbLocation += '/subscriptions.db'
} else {
dbLocation = 'subscriptions.db'
}
const subDb = new Datastore({
filename: dbLocation,
autoload: true
})
const state = { const state = {
subscriptions: [] subscriptions: [],
profileSubscriptions: {
activeProfile: 0,
videoList: []
}
} }
const mutations = { const getters = {
addSubscription (state, payload) { getSubscriptions: () => {
state.subscriptions.push(payload) return state.subscriptions
}, },
setSubscriptions (state, payload) { getProfileSubscriptions: () => {
state.subscriptions = payload return state.profileSubscriptions
} }
} }
const actions = { const actions = {
addSubscriptions ({ commit }, payload) { updateSubscriptions ({ commit }, subscriptions) {
subDb.insert(payload, (err, payload) => { commit('setSubscriptions', subscriptions)
if (!err) {
commit('addSubscription', payload)
}
})
}, },
getSubscriptions ({ commit }, payload) { updateProfileSubscriptions ({ commit }, subscriptions) {
subDb.find({}, (err, payload) => { commit('setProfileSubscriptions', subscriptions)
if (!err) {
commit('setSubscriptions', payload)
}
})
},
removeSubscription ({ commit }, channelId) {
subDb.remove({ channelId: channelId }, {}, () => {
commit('setSubscriptions', this.state.subscriptions.filter(sub => sub.channelId !== channelId))
})
} }
} }
const getters = {}
const mutations = {
setSubscriptions (state, subscriptions) {
state.subscriptions = subscriptions
},
setProfileSubscriptions (state, profileSubscriptions) {
state.profileSubscriptions = profileSubscriptions
}
}
export default { export default {
state, state,
getters, getters,

View File

@ -5,6 +5,8 @@ const state = {
sessionSearchHistory: [], sessionSearchHistory: [],
popularCache: null, popularCache: null,
trendingCache: null, trendingCache: null,
showProgressBar: false,
progressBarPercentage: 0,
searchSettings: { searchSettings: {
sortBy: 'relevance', sortBy: 'relevance',
time: '', time: '',
@ -76,10 +78,22 @@ const getters = {
getColorValues () { getColorValues () {
return state.colorValues return state.colorValues
},
getShowProgressBar () {
return state.showProgressBar
},
getProgressBarPercentage () {
return state.progressBarPercentage
} }
} }
const actions = { const actions = {
updateShowProgressBar ({ commit }, value) {
commit('setShowProgressBar', value)
},
getRandomColorClass () { getRandomColorClass () {
const randomInt = Math.floor(Math.random() * state.colorClasses.length) const randomInt = Math.floor(Math.random() * state.colorClasses.length)
return state.colorClasses[randomInt] return state.colorClasses[randomInt]
@ -105,6 +119,33 @@ const actions = {
} }
}, },
calculatePublishedDate(_, publishedText) {
const date = new Date()
const textSplit = publishedText.split(' ')
const timeFrame = textSplit[1]
const timeAmount = parseInt(textSplit[0])
let timeSpan = null
if (timeFrame.indexOf('second') > -1) {
timeSpan = timeAmount * 1000
} else if (timeFrame.indexOf('minute') > -1) {
timeSpan = timeAmount * 60000
} else if (timeFrame.indexOf('hour') > -1) {
timeSpan = timeAmount * 3600000
} else if (timeFrame.indexOf('day') > -1) {
timeSpan = timeAmount * 86400000
} else if (timeFrame.indexOf('week') > -1) {
timeSpan = timeAmount * 604800000
} else if (timeFrame.indexOf('month') > -1) {
timeSpan = timeAmount * 2592000000
} else if (timeFrame.indexOf('year') > -1) {
timeSpan = timeAmount * 31556952000
}
return date.getTime() - timeSpan
},
getVideoIdFromUrl (_, url) { getVideoIdFromUrl (_, url) {
/** @type {URL} */ /** @type {URL} */
let urlObject let urlObject
@ -296,6 +337,14 @@ const mutations = {
state.isSideNavOpen = !state.isSideNavOpen state.isSideNavOpen = !state.isSideNavOpen
}, },
setShowProgressBar (state, value) {
state.showProgressBar = value
},
setProgressBarPercentage (state, value) {
state.progressBarPercentage = value
},
setSessionSearchHistory (state, history) { setSessionSearchHistory (state, history) {
state.sessionSearchHistory = history state.sessionSearchHistory = history
}, },

View File

@ -78,6 +78,34 @@ export default Vue.extend({
return this.$store.getters.getSessionSearchHistory return this.$store.getters.getSessionSearchHistory
}, },
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
isSubscribed: function () {
const subIndex = this.profileList[this.activeProfile].subscriptions.findIndex((channel) => {
return channel.id === this.id
})
if (subIndex === -1) {
return false
} else {
return true
}
},
subscribedText: function () {
if (this.isSubscribed) {
return this.$t('Channel.Unsubscribe').toUpperCase()
} else {
return this.$t('Channel.Subscribe').toUpperCase()
}
},
videoSelectNames: function () { videoSelectNames: function () {
return [ return [
this.$t('Channel.Videos.Sort Types.Newest'), this.$t('Channel.Videos.Sort Types.Newest'),
@ -408,7 +436,74 @@ export default Vue.extend({
}, },
handleSubscription: function () { handleSubscription: function () {
console.log('TODO: Channel handleSubscription') const currentProfile = JSON.parse(JSON.stringify(this.profileList[this.activeProfile]))
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
if (this.isSubscribed) {
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
return channel.id !== this.id
})
this.updateProfile(currentProfile)
this.showToast({
message: 'Channel has been removed from your subscriptions'
})
if (this.activeProfile === 0) {
// Check if a subscription exists in a different profile.
// Remove from there as well.
let duplicateSubscriptions = 0
this.profileList.forEach((profile) => {
if (profile._id === 'allChannels') {
return
}
const parsedProfile = JSON.parse(JSON.stringify(profile))
const index = parsedProfile.subscriptions.findIndex((channel) => {
return channel.id === this.id
})
if (index !== -1) {
duplicateSubscriptions++
parsedProfile.subscriptions = parsedProfile.subscriptions.filter((x) => {
return x.id !== this.channelId
})
this.updateProfile(parsedProfile)
}
})
if (duplicateSubscriptions > 0) {
this.showToast({
message: `Removed subscription from ${duplicateSubscriptions} other channel(s)`
})
}
}
} else {
const subscription = {
id: this.id,
name: this.channelName,
thumbnail: this.thumbnailUrl
}
currentProfile.subscriptions.push(subscription)
this.updateProfile(currentProfile)
this.showToast({
message: 'Added channel to your subscriptions'
})
if (this.activeProfile !== 0) {
const index = primaryProfile.subscriptions.findIndex((channel) => {
return channel.id === this.id
})
if (index === -1) {
primaryProfile.subscriptions.push(subscription)
this.updateProfile(primaryProfile)
}
}
}
}, },
handleFetchMore: function () { handleFetchMore: function () {
@ -549,7 +644,8 @@ export default Vue.extend({
}, },
...mapActions([ ...mapActions([
'showToast' 'showToast',
'updateProfile'
]) ])
} }
}) })

View File

@ -40,7 +40,7 @@
</span> </span>
</div> </div>
<ft-button <ft-button
:label="$t('Channel.Subscribe')" :label="subscribedText"
background-color="var(--primary-color)" background-color="var(--primary-color)"
text-color="var(--text-with-main-color)" text-color="var(--text-with-main-color)"
class="subscribeButton" class="subscribeButton"

View File

@ -107,9 +107,6 @@ export default Vue.extend({
return video return video
}) })
return video
})
this.isLoading = false this.isLoading = false
}).catch((err) => { }).catch((err) => {
console.log(err) console.log(err)

View File

@ -16,6 +16,12 @@
margin-bottom: 30px; margin-bottom: 30px;
} }
.colorOptions {
max-width: 1000px;
margin: 0 auto;
margin-bottom: 30px;
}
.colorOption { .colorOption {
width: 100px; width: 100px;
height: 100px; height: 100px;

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import { mapActions } from 'vuex' import { mapActions } from 'vuex'
import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue' import FtCard from '../../components/ft-card/ft-card.vue'
import FtPrompt from '../../components/ft-prompt/ft-prompt.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtInput from '../../components/ft-input/ft-input.vue' import FtInput from '../../components/ft-input/ft-input.vue'
import FtButton from '../../components/ft-button/ft-button.vue' import FtButton from '../../components/ft-button/ft-button.vue'
@ -11,6 +12,7 @@ export default Vue.extend({
components: { components: {
'ft-loader': FtLoader, 'ft-loader': FtLoader,
'ft-card': FtCard, 'ft-card': FtCard,
'ft-prompt': FtPrompt,
'ft-flex-box': FtFlexBox, 'ft-flex-box': FtFlexBox,
'ft-input': FtInput, 'ft-input': FtInput,
'ft-button': FtButton 'ft-button': FtButton
@ -18,12 +20,18 @@ export default Vue.extend({
data: function () { data: function () {
return { return {
isLoading: false, isLoading: false,
showDeletePrompt: false,
deletePromptLabel: '',
isNew: false, isNew: false,
profileId: '', profileId: '',
profileName: '', profileName: '',
profileBgColor: '', profileBgColor: '',
profileTextColor: '', profileTextColor: '',
profileSubscriptions: [] profileSubscriptions: [],
deletePromptValues: [
'yes',
'no'
]
} }
}, },
computed: { computed: {
@ -32,6 +40,18 @@ export default Vue.extend({
}, },
profileInitial: function () { profileInitial: function () {
return this.profileName.slice(0, 1).toUpperCase() return this.profileName.slice(0, 1).toUpperCase()
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
defaultProfile: function () {
return this.$store.getters.getDefaultProfile
},
deletePromptNames: function () {
return [
this.$t('Yes'),
this.$t('No')
]
} }
}, },
watch: { watch: {
@ -39,12 +59,16 @@ export default Vue.extend({
this.profileTextColor = await this.calculateColorLuminance(val) this.profileTextColor = await this.calculateColorLuminance(val)
} }
}, },
mounted: function () { mounted: async function () {
this.isLoading = true this.isLoading = true
const profileType = this.$route.name const profileType = this.$route.name
this.deletePromptLabel = 'Are you sure you want to delete this profile? All subscriptions in this profile will also be deleted.'
if (profileType === 'newProfile') { if (profileType === 'newProfile') {
this.isNew = true this.isNew = true
this.profileBgColor = await this.getRandomColor()
this.isLoading = false
} else { } else {
this.isNew = false this.isNew = false
this.profileId = this.$route.params.id this.profileId = this.$route.params.id
@ -52,7 +76,14 @@ export default Vue.extend({
console.log(this.$route.name) console.log(this.$route.name)
this.grabProfileInfo(this.profileId).then((profile) => { this.grabProfileInfo(this.profileId).then((profile) => {
console.log(profile) if (profile === null) {
this.showToast({
message: 'Profile could not be found'
})
this.$router.push({
path: '/settings/profile/'
})
}
this.profileName = profile.name this.profileName = profile.name
this.profileBgColor = profile.bgColor this.profileBgColor = profile.bgColor
this.profileTextColor = profile.textColor this.profileTextColor = profile.textColor
@ -62,7 +93,25 @@ export default Vue.extend({
} }
}, },
methods: { methods: {
openDeletePrompt: function () {
this.showDeletePrompt = true
},
handleDeletePrompt: function (response) {
if (response === 'yes') {
this.deleteProfile()
} else {
this.showDeletePrompt = false
}
},
saveProfile: function () { saveProfile: function () {
if (this.profileName === '') {
this.showToast({
message: 'Your profile name cannot be empty'
})
return
}
const profile = { const profile = {
name: this.profileName, name: this.profileName,
bgColor: this.profileBgColor, bgColor: this.profileBgColor,
@ -77,8 +126,44 @@ export default Vue.extend({
console.log(profile) console.log(profile)
this.updateProfile(profile) this.updateProfile(profile)
if (this.isNew) {
this.showToast({
message: 'Profile has been created'
})
this.$router.push({
path: '/settings/profile/'
})
} else {
this.showToast({
message: 'Profile has been updated'
})
}
},
setDefaultProfile: function () {
this.updateDefaultProfile(this.profileId)
this.showToast({ this.showToast({
message: 'Profile has been updated' message: `Your default profile has been set to ${this.profileName}`
})
},
deleteProfile: function () {
this.removeProfile(this.profileId)
this.showToast({
message: `Removed ${this.profileName} from your profiles`
})
if (this.defaultProfile === this.profileId) {
this.updateDefaultProfile('allChannels')
this.showToast({
message: 'Your default profile has been set your Primary profile'
})
}
if (this.activeProfile._id === this.profileId) {
this.updateActiveProfile('allChannels')
}
this.$router.push({
path: '/settings/profile/'
}) })
}, },
@ -86,7 +171,11 @@ export default Vue.extend({
'showToast', 'showToast',
'grabProfileInfo', 'grabProfileInfo',
'updateProfile', 'updateProfile',
'calculateColorLuminance' 'removeProfile',
'updateDefaultProfile',
'updateActiveProfile',
'calculateColorLuminance',
'getRandomColor'
]) ])
} }
}) })

View File

@ -20,7 +20,7 @@
</ft-flex-box> </ft-flex-box>
<h3>{{ $t("Profile.Color Picker") }}</h3> <h3>{{ $t("Profile.Color Picker") }}</h3>
<ft-flex-box <ft-flex-box
class="bottomMargin" class="bottomMargin colorOptions"
> >
<div <div
v-for="(color, index) in colorValues" v-for="(color, index) in colorValues"
@ -69,22 +69,37 @@
</ft-flex-box> </ft-flex-box>
<ft-flex-box> <ft-flex-box>
<ft-button <ft-button
v-if="isNew"
:label="$t('Profile.Create Profile')"
@click="saveProfile"
/>
<ft-button
v-if="!isNew"
:label="$t('Profile.Update Profile')" :label="$t('Profile.Update Profile')"
@click="saveProfile" @click="saveProfile"
/> />
<ft-button <ft-button
v-if="!isNew"
:label="$t('Profile.Make Default Profile')" :label="$t('Profile.Make Default Profile')"
@click="saveProfile" @click="setDefaultProfile"
/> />
<ft-button <ft-button
v-if="profileId !== 'allChannels' && !isNew"
:label="$t('Profile.Delete Profile')" :label="$t('Profile.Delete Profile')"
text-color="var(--text-with-main-color)" text-color="var(--text-with-main-color)"
background-color="var(--primary-color)" background-color="var(--primary-color)"
@click="saveProfile" @click="openDeletePrompt"
/> />
</ft-flex-box> </ft-flex-box>
</ft-card> </ft-card>
</div> </div>
<ft-prompt
v-if="showDeletePrompt"
:label="deletePromptLabel"
:option-names="deletePromptNames"
:option-values="deletePromptValues"
@click="handleDeletePrompt"
/>
</div> </div>
</template> </template>

View File

@ -8,6 +8,12 @@
color: var(--tertiary-text-color); color: var(--tertiary-text-color);
} }
.profileList {
max-width: 1000px;
margin: 0 auto;
margin-bottom: 10px;
}
@media only screen and (max-width: 680px) { @media only screen and (max-width: 680px) {
.card { .card {
width: 90%; width: 90%;

View File

@ -2,13 +2,15 @@ import Vue from 'vue'
import FtCard from '../../components/ft-card/ft-card.vue' import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtProfileBubble from '../../components/ft-profile-bubble/ft-profile-bubble.vue' import FtProfileBubble from '../../components/ft-profile-bubble/ft-profile-bubble.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
export default Vue.extend({ export default Vue.extend({
name: 'ProfileSettings', name: 'ProfileSettings',
components: { components: {
'ft-card': FtCard, 'ft-card': FtCard,
'ft-flex-box': FtFlexBox, 'ft-flex-box': FtFlexBox,
'ft-profile-bubble': FtProfileBubble 'ft-profile-bubble': FtProfileBubble,
'ft-button': FtButton
}, },
computed: { computed: {
profileList: function () { profileList: function () {
@ -17,5 +19,12 @@ export default Vue.extend({
}, },
mounted: function () { mounted: function () {
console.log(this.profileList) console.log(this.profileList)
},
methods: {
newProfile: function () {
this.$router.push({
path: `/settings/profile/new/`
})
}
} }
}) })

View File

@ -2,7 +2,9 @@
<div> <div>
<ft-card class="card"> <ft-card class="card">
<h3>{{ $t("Profile.Profile Manager") }}</h3> <h3>{{ $t("Profile.Profile Manager") }}</h3>
<ft-flex-box> <ft-flex-box
class="profileList"
>
<ft-profile-bubble <ft-profile-bubble
v-for="(profile, index) in profileList" v-for="(profile, index) in profileList"
:key="index" :key="index"
@ -12,6 +14,12 @@
:text-color="profile.textColor" :text-color="profile.textColor"
/> />
</ft-flex-box> </ft-flex-box>
<ft-flex-box>
<ft-button
label="Create New Profile"
@click="newProfile"
/>
</ft-flex-box>
</ft-card> </ft-card>
</div> </div>
</template> </template>

View File

@ -1,15 +1,164 @@
import Vue from 'vue' import Vue from 'vue'
import { mapActions, mapMutations } from 'vuex'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue' import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import ytch from 'yt-channel-info'
export default Vue.extend({ export default Vue.extend({
name: 'Subscriptions', name: 'Subscriptions',
components: { components: {
'ft-loader': FtLoader,
'ft-card': FtCard, 'ft-card': FtCard,
'ft-flex-box': FtFlexBox, 'ft-flex-box': FtFlexBox,
'ft-element-list': FtElementList 'ft-element-list': FtElementList
}, },
data: function () {
return {
isLoading: false,
dataLimit: 100,
videoList: []
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
profileList: function () {
return this.$store.getters.getProfileList
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
profileSubscriptions: function () {
return this.$store.getters.getProfileSubscriptions
},
activeSubscriptionList: function () {
return this.profileList[this.activeProfile].subscriptions
},
allSubscriptionsList: function () {
return this.profileList[0].subscriptions
},
sortedVideoList: function () {
const profileSubscriptions = JSON.parse(JSON.stringify(this.profileSubscriptions))
return profileSubscriptions.videoList.sort((a, b) => {
if (a.title.toLowerCase() > b.title.toLowerCase()) {
return -1
}
if (a.title.toLowerCase() < b.title.toLowerCase()) {
return 1
}
console.log(a.title)
return 0
})
}
},
mounted: function () { mounted: function () {
setTimeout(() => {
this.fetchActiveSubscriptionsLocal()
}, 1000)
},
methods: {
fetchActiveSubscriptionsLocal: function () {
if (this.activeSubscriptionList.length === 0) {
return
}
this.isLoading = true
this.updateShowProgressBar(true)
let videoList = []
let channelCount = 0
this.activeSubscriptionList.forEach(async (channel) => {
const videos = await this.getChannelVideosLocalScraper(channel.id)
console.log(videos)
videoList = videoList.concat(videos)
channelCount++
const percentageComplete = (channelCount / this.activeSubscriptionList.length) * 100
this.setProgressBarPercentage(percentageComplete)
if (channelCount === this.activeSubscriptionList.length) {
videoList = await Promise.all(videoList.sort((a, b) => {
return b.publishedDate - a.publishedDate
}))
const profileSubscriptions = {
activeProfile: this.activeProfile,
videoList: videoList
}
this.updateProfileSubscriptions(profileSubscriptions)
this.isLoading = false
this.updateShowProgressBar(false)
}
})
},
getChannelVideosLocalScraper: function (channelId) {
return new Promise((resolve, reject) => {
ytch.getChannelVideos(channelId, 'latest').then(async (response) => {
const videos = await Promise.all(response.items.map(async (video) => {
video.publishedDate = await this.calculatePublishedDate(video.publishedText)
return video
}))
resolve(videos)
}).catch((err) => {
console.log(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
this.showToast({
message: `${errorMessage}: ${err}`,
time: 10000,
action: () => {
navigator.clipboard.writeText(err)
}
})
resolve([])
})
})
},
getChannelVideosLocalRSS: function (channelId) {
console.log('TODO')
},
fetchActiveSubscriptionsInvidious: function () {
console.log('TODO')
},
getChannelVideosInvidiousScraper: function (channelId) {
console.log('TODO')
},
getChannelVideosInvidiousRSS: function (channelId) {
console.log('TODO')
},
...mapActions([
'showToast',
'updateShowProgressBar',
'updateProfileSubscriptions',
'calculatePublishedDate'
]),
...mapMutations([
'setProgressBarPercentage'
])
} }
}) })

View File

@ -1,12 +1,35 @@
<template> <template>
<div> <div>
<ft-card class="card"> <ft-loader
v-if="isLoading"
:fullscreen="true"
/>
<ft-card
v-else
class="card"
>
<h3>{{ $t("Subscriptions.Subscriptions") }}</h3> <h3>{{ $t("Subscriptions.Subscriptions") }}</h3>
<ft-flex-box> <ft-flex-box
v-if="profileSubscriptions.videoList.length === 0"
>
<p class="message"> <p class="message">
{{ $t("This part of the app is not ready yet. Come back later when progress has been made.") }} {{ $t("History['Your history list is currently empty.']") }}
</p> </p>
</ft-flex-box> </ft-flex-box>
<ft-element-list
v-else
:data="profileSubscriptions.videoList"
/>
<ft-flex-box
v-if="false"
>
<ft-button
label="Load More"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="increaseLimit"
/>
</ft-flex-box>
</ft-card> </ft-card>
</div> </div>
</template> </template>

View File

@ -335,6 +335,8 @@ Video:
Dec: Dec Dec: Dec
Second: Second Second: Second
Seconds: Seconds Seconds: Seconds
Minute: Minute
Minutes: Minutes
Hour: Hour Hour: Hour
Hours: Hours Hours: Hours
Day: Day Day: Day