Merge pull request #172 from h3poteto/iss-132

closes #132 Create account preferences page
This commit is contained in:
AkiraFukushima 2018-04-01 22:09:03 +09:00 committed by GitHub
commit 1527f1dcaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 426 additions and 33 deletions

View File

@ -75,6 +75,21 @@ export default class Account {
)
})
}
removeAccount (id) {
return new Promise((resolve, reject) => {
this.db.remove(
{
_id: id
},
{ multi: true },
(err, numRemoved) => {
if (err) return reject(err)
resolve(numRemoved)
}
)
})
}
}
class EmptyRecordError {

View File

@ -78,6 +78,16 @@ function createWindow () {
{
type: 'separator'
},
{
label: 'Preferences...',
accelerator: 'CmdOrCtrl+,',
click: () => {
mainWindow.webContents.send('open-preferences')
}
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
@ -288,6 +298,17 @@ ipcMain.on('update-account', (event, acct) => {
})
})
ipcMain.on('remove-account', (event, id) => {
const account = new Account(db)
account.removeAccount(id)
.then(() => {
event.sender.send('response-remove-account')
})
.catch((err) => {
event.sender.send('error-remove-account', err)
})
})
// streaming
let userStreaming = null

View File

@ -6,11 +6,28 @@
<script>
export default {
name: 'Whalebird'
name: 'Whalebird',
created () {
this.$store.dispatch('App/watchShortcutsEvents')
},
destroyed () {
this.$store.dispatch('App/removeShortcutsEvents')
}
}
</script>
<style lang="scss">
body { font-family: 'Noto Sans', sans-serif; }
html, body, #app, #global_header {
height: 100%;
margin: 0;
}
p {
margin: 8px 0;
}
.clearfix:after {
content:" ";
display:block;

View File

@ -50,18 +50,12 @@ export default {
}
</script>
<style lang="scss">
body { font-family: 'Noto Sans', 'Source Sans Pro', sans-serif; }
html, body, #app, #authorize {
height: 100%;
margin: 0;
}
#authorize {
<style lang="scss" scoped>
#authorize /deep/ {
background-color: #292f3f;
color: #ffffff;
text-align: center;
min-height: 100%;
.close {
text-align: right;

View File

@ -63,19 +63,8 @@ export default {
}
</script>
<style lang="scss">
body { font-family: 'Noto Sans', sans-serif; }
html, body, #app, #global_header {
height: 100%;
margin: 0;
}
p {
margin: 8px 0;
}
#global_header {
<style lang="scss" scoped>
#global_header /deep/ {
.account-menu {
height: 100%;
position: fixed;

View File

@ -30,15 +30,8 @@ export default {
}
</script>
<style lang="scss">
body { font-family: 'Noto Sans', 'Source Sans Pro', sans-serif; }
html, body, #app {
height: 100%;
margin: 0;
}
#login {
<style lang="scss" scoped>
#login /deep/ {
background-color: #292f3f;
color: #ffffff;
text-align: center;

View File

@ -0,0 +1,99 @@
<template>
<el-container id="preferences">
<el-header class="header">
<el-row>
<el-col :span="23">
<h3>Preferences</h3>
</el-col>
<el-col :span="1">
<el-button type="text" icon="el-icon-close" @click="close" class="close-button"></el-button>
</el-col>
</el-row>
</el-header>
<el-container>
<el-aside width="240px" class="menu">
<el-menu
:default-active="defaultActive"
class="setting-menu"
:route="true">
<el-menu-item index="1" :route="{path: '/preferences/general'}" @click="general">
<icon name="cog" class="icon" scale="1.3"></icon>
<span>General</span>
</el-menu-item>
<el-menu-item index="2" :route="{path: '/preferences/account'}" @click="account">
<icon name="user" class="icon" scale="1.3"></icon>
<span>Account</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'preferences',
computed: {
...mapState({
defaultActive: state => state.Preferences.defaultActive
})
},
methods: {
close () {
this.$router.push('/')
},
general () {
this.$router.push('/preferences/general')
},
account () {
this.$router.push('/preferences/account')
}
}
}
</script>
<style lang="scss" scoped>
#preferences {
height: 100%;
}
.header {
text-align: center;
border-bottom: 1px solid #dcdfe6;
.close-button {
font-size: 24px;
}
}
.menu {
text-align: right;
padding-left: 24px;
.setting-menu /deep/ {
height: 100%;
.icon {
margin-right: 9px;
}
.el-menu-item {
.icon {
color: #909399;
}
}
.is-active {
.icon {
color: #409eff;
}
}
}
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div id="account">
<h2>Account</h2>
<div class="connected-account">
<h3>Connected Accounts</h3>
<template>
<el-table
:data="accounts"
stripe
empty-text="No accounts"
style="width: 100%"
v-loading="accountLoading">
<el-table-column
prop="username"
label="Username"
width="240">
</el-table-column>
<el-table-column
prop="domain"
label="Domain">
</el-table-column>
<el-table-column
label="Association">
<template slot-scope="scope">
<el-button
@click.native.prevent="removeAccount(scope.$index, accounts)"
type="text">
<i class="el-icon-close"></i> Remove association
</el-button>
</template>
</el-table-column>
</el-table>
</template>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'account',
data () {
return {
openRemoveDialog: false
}
},
computed: {
...mapState({
accounts: state => state.Preferences.Account.accounts,
accountLoading: state => state.Preferences.Account.accountLoading
})
},
created () {
this.$store.commit('Preferences/changeActive', '2')
this.loadAccounts()
},
methods: {
async loadAccounts () {
this.$store.commit('Preferences/Account/updateAccountLoading', true)
try {
const accounts = await this.$store.dispatch('Preferences/Account/loadAccounts')
await this.$store.dispatch('Preferences/Account/fetchUsername', accounts)
this.$store.commit('Preferences/Account/updateAccountLoading', false)
} catch (err) {
this.$store.commit('Preferences/Account/updateAccountLoading', false)
return this.$message({
message: 'Failed to load accounts',
type: 'error'
})
}
},
removeAccount (index, accounts) {
this.$store.dispatch('Preferences/Account/removeAccount', accounts[index])
.then(() => {
this.loadAccounts()
})
.catch(() => {
this.$message({
message: 'Failed to remove the association',
type: 'error'
})
})
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,14 @@
<template>
<div id="general">
general
</div>
</template>
<script>
export default {
name: 'general'
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -15,6 +15,23 @@ export default new Router({
name: 'authorize',
component: require('@/components/Authorize').default
},
{
path: '/preferences/',
name: 'preferences',
component: require('@/components/Preferences').default,
children: [
{
path: 'general',
name: 'general',
component: require('@/components/Preferences/General').default
},
{
path: 'account',
name: 'account',
component: require('@/components/Preferences/Account').default
}
]
},
{
path: '/',
name: 'global-header',

20
src/renderer/store/App.js Normal file
View File

@ -0,0 +1,20 @@
import { ipcRenderer } from 'electron'
import router from '../router'
const App = {
namespaced: true,
state: {},
mutations: {},
actions: {
watchShortcutsEvents () {
ipcRenderer.on('open-preferences', (event) => {
router.push('/preferences/account')
})
},
removeShortcutsEvents () {
ipcRenderer.removeAllListeners('open-preferences')
}
}
}
export default App

View File

@ -0,0 +1,20 @@
import General from './Preferences/General'
import Account from './Preferences/Account'
const Preferences = {
namespaced: true,
modules: {
General,
Account
},
state: {
defaultActive: '1'
},
mutations: {
changeActive (state, value) {
state.defaultActive = value
}
}
}
export default Preferences

View File

@ -0,0 +1,92 @@
import { ipcRenderer } from 'electron'
import Mastodon from 'mastodon-api'
const Account = {
namespaced: true,
state: {
accounts: [],
accountLoading: false
},
mutations: {
updateAccounts (state, accounts) {
state.accounts = accounts
},
mergeAccounts (state, accounts) {
// TODO: Save username in local db after authorize.
// This function can not support if user add multiple accounts which are exist in same domain.
// So when username is saved in local db, please compare with reference to username@domain.
state.accounts = state.accounts.map((a) => {
let account = a
accounts.map((acct) => {
if (acct.domain === a.domain) {
account = acct
}
})
return account
})
},
updateAccountLoading (state, value) {
state.accountLoading = value
}
},
actions: {
loadAccounts ({ commit }) {
return new Promise((resolve, reject) => {
ipcRenderer.send('list-accounts', 'list')
ipcRenderer.once('error-list-accounts', (event, err) => {
ipcRenderer.removeAllListeners('response-list-accounts')
reject(err)
})
ipcRenderer.once('response-list-accounts', (event, accounts) => {
ipcRenderer.removeAllListeners('error-list-accounts')
commit('updateAccounts', accounts)
resolve(accounts)
})
})
},
fetchUsername ({ dispatch, commit }, accounts) {
return new Promise((resolve, reject) => {
dispatch('fetchAllAccounts', accounts)
.then((accounts) => {
commit('mergeAccounts', accounts)
resolve(accounts)
})
.catch((err) => {
reject(err)
})
})
},
fetchAllAccounts ({ commit }, accounts) {
return Promise.all(accounts.map((account) => {
return new Promise((resolve, reject) => {
const client = new Mastodon(
{
access_token: account.accessToken,
api_url: account.baseURL + '/api/v1'
})
client.get('/accounts/verify_credentials', (err, data, res) => {
if (err) return reject(err)
// The response doesn't have domain, so I cann't confirm that response and account is same.
// Therefore I merge account.
resolve(Object.assign(data, account))
})
})
}))
},
removeAccount ({ commit }, account) {
return new Promise((resolve, reject) => {
ipcRenderer.send('remove-account', account._id)
ipcRenderer.once('error-remove-account', (event, err) => {
ipcRenderer.removeAllListeners('response-remove-account')
reject(err)
})
ipcRenderer.once('response-remove-account', (event) => {
ipcRenderer.removeAllListeners('error-remove-account')
resolve()
})
})
}
}
}
export default Account

View File

@ -0,0 +1,8 @@
const General = {
namespaced: true,
state: {},
mutations: {},
actions: {}
}
export default General

View File

@ -2,10 +2,12 @@ import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
import App from './App'
import GlobalHeader from './GlobalHeader'
import Login from './Login'
import Authorize from './Authorize'
import TimelineSpace from './TimelineSpace'
import Preferences from './Preferences'
Vue.use(Vuex)
@ -15,9 +17,11 @@ export default new Vuex.Store({
? [createLogger()]
: [],
modules: {
App,
GlobalHeader,
Login,
Authorize,
TimelineSpace
TimelineSpace,
Preferences
}
})