refs #884 Add request loading circle in header menu

This commit is contained in:
AkiraFukushima 2019-05-07 22:40:46 +09:00
parent 0eb97c9395
commit 327f621791
6 changed files with 205 additions and 78 deletions

View File

@ -14,7 +14,8 @@ const list: List = {
const state = (): HeaderMenuState => {
return {
title: 'Home',
reload: false
reload: false,
loading: false
}
}

View File

@ -6,7 +6,8 @@ describe('TimelineSpace/HeaderMenu', () => {
beforeEach(() => {
state = {
title: 'Home',
reload: false
reload: false,
loading: false
}
})
describe('changeReload', () => {

View File

@ -0,0 +1,57 @@
<svg class="lds-spinner" width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%;"><g transform="rotate(0 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9285714285714286s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(25.714285714285715 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8571428571428571s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(51.42857142857143 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.7857142857142857s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(77.14285714285714 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.7142857142857143s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(102.85714285714286 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6428571428571429s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(128.57142857142858 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5714285714285714s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(154.28571428571428 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(180 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.42857142857142855s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(205.71428571428572 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.35714285714285715s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(231.42857142857142 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.2857142857142857s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(257.14285714285717 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.21428571428571427s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(282.85714285714283 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.14285714285714285s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(308.57142857142856 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.07142857142857142s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(334.2857142857143 50 50)">
<rect x="47" y="3.5" rx="47" ry="3.5" width="6" height="23" fill="#93dbe9">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
</rect>
</g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,59 +1,66 @@
<template>
<nav id="header_menu" :aria-label="title">
<div class="channel">
<h1>{{ title }}</h1>
</div>
<div class="tools">
<el-button v-if="!pleroma" type="text" class="action" @click="switchStreaming" :title="$t('header_menu.switch_streaming')">
<svg :class="useWebsocket ? 'websocket' : 'not-websocket'" width="25" height="18" viewBox="0 0 256 193" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M192.44 144.645h31.78V68.339l-35.805-35.804-22.472 22.472 26.497 26.497v63.14zm31.864 15.931H113.452L86.954 134.08l11.237-11.236 21.885 21.885h45.028l-44.357-44.441 11.32-11.32 44.357 44.358V88.296l-21.801-21.801 11.152-11.153L110.685 0H0l31.696 31.696v.084H97.436l23.227 23.227-33.96 33.96L63.476 65.74V47.712h-31.78v31.193l55.007 55.007L64.314 156.3l35.805 35.805H256l-31.696-31.529z" /></svg>
</el-button>
<el-button type="text" class="action" @click="openNewTootModal" :title="$t('header_menu.new_toot')">
<icon name="regular/edit" scale="1.1"></icon>
</el-button>
<el-button v-show="reloadable()" type="text" class="action" @click="reload" :title="$t('header_menu.reload')">
<icon name="sync-alt"></icon>
</el-button>
<el-popover
placement="left-start"
width="320"
popper-class="theme-popover"
trigger="click"
v-model="filterVisible">
<div>
<el-form role="form" label-position="left" label-width="125px" size="medium">
<el-form-item for="filter" :label="$t('header_menu.filter.title')">
<div class="el-input">
<input
id="filter"
class="el-input__inner"
v-model="filter"
:placeholder="$t('header_menu.filter.placeholder')"
v-shortkey.avoid
:aria-label="$t('header_menu.filter.placeholder')"
:title="$t('header_menu.filter.placeholder')"
>
</div>
</el-form-item>
<el-form-item for="show-reblogs" :label="$t('header_menu.filter.show_reblogs')" v-if="extrasFilterable()">
<el-checkbox id="show-reblogs" v-model="showReblogs"></el-checkbox>
</el-form-item>
<el-form-item for="show-replies" :label="$t('header_menu.filter.show_replies')" v-if="extrasFilterable()">
<el-checkbox id="show-replies" v-model="showReplies"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="applyFilter(filter)">{{ $t('header_menu.filter.apply') }}</el-button>
</el-form-item>
</el-form>
</div>
<el-button v-show="filterable()" slot="reference" type="text" class="action" :title="$t('header_menu.filter.title')">
<icon name="sliders-h"></icon>
<nav id="header_menu" :aria-label="title">
<div class="channel">
<h1>{{ title }}</h1>
</div>
<div class="tools">
<img src="../../assets/images/loading-spinner-wide.svg" v-show="loading" class="header-loading" />
<el-button v-if="!pleroma" type="text" class="action" @click="switchStreaming" :title="$t('header_menu.switch_streaming')">
<svg
:class="useWebsocket ? 'websocket' : 'not-websocket'"
width="25"
height="18"
viewBox="0 0 256 193"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
>
<path
d="M192.44 144.645h31.78V68.339l-35.805-35.804-22.472 22.472 26.497 26.497v63.14zm31.864 15.931H113.452L86.954 134.08l11.237-11.236 21.885 21.885h45.028l-44.357-44.441 11.32-11.32 44.357 44.358V88.296l-21.801-21.801 11.152-11.153L110.685 0H0l31.696 31.696v.084H97.436l23.227 23.227-33.96 33.96L63.476 65.74V47.712h-31.78v31.193l55.007 55.007L64.314 156.3l35.805 35.805H256l-31.696-31.529z"
/>
</svg>
</el-button>
</el-popover>
<el-button type="text" class="action" @click="settings" :title="$t('header_menu.settings')">
<icon name="cog" scale="1.1"></icon>
</el-button>
</div>
</nav>
<el-button type="text" class="action" @click="openNewTootModal" :title="$t('header_menu.new_toot')">
<icon name="regular/edit" scale="1.1"></icon>
</el-button>
<el-button v-show="reloadable()" type="text" class="action" @click="reload" :title="$t('header_menu.reload')">
<icon name="sync-alt"></icon>
</el-button>
<el-popover placement="left-start" width="320" popper-class="theme-popover" trigger="click" v-model="filterVisible">
<div>
<el-form role="form" label-position="left" label-width="125px" size="medium">
<el-form-item for="filter" :label="$t('header_menu.filter.title')">
<div class="el-input">
<input
id="filter"
class="el-input__inner"
v-model="filter"
:placeholder="$t('header_menu.filter.placeholder')"
v-shortkey.avoid
:aria-label="$t('header_menu.filter.placeholder')"
:title="$t('header_menu.filter.placeholder')"
/>
</div>
</el-form-item>
<el-form-item for="show-reblogs" :label="$t('header_menu.filter.show_reblogs')" v-if="extrasFilterable()">
<el-checkbox id="show-reblogs" v-model="showReblogs"></el-checkbox>
</el-form-item>
<el-form-item for="show-replies" :label="$t('header_menu.filter.show_replies')" v-if="extrasFilterable()">
<el-checkbox id="show-replies" v-model="showReplies"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="applyFilter(filter)">{{ $t('header_menu.filter.apply') }}</el-button>
</el-form-item>
</el-form>
</div>
<el-button v-show="filterable()" slot="reference" type="text" class="action" :title="$t('header_menu.filter.title')">
<icon name="sliders-h"></icon>
</el-button>
</el-popover>
<el-button type="text" class="action" @click="settings" :title="$t('header_menu.settings')">
<icon name="cog" scale="1.1"></icon>
</el-button>
</div>
</nav>
</template>
<script>
@ -61,7 +68,7 @@ import { mapState } from 'vuex'
export default {
name: 'header-menu',
data () {
data() {
return {
filter: '',
filterVisible: false,
@ -71,28 +78,30 @@ export default {
},
computed: {
...mapState('TimelineSpace/HeaderMenu', {
title: state => state.title
title: state => state.title,
loading: state => state.loading
}),
...mapState('TimelineSpace', {
useWebsocket: state => state.useWebsocket,
pleroma: state => state.pleroma
})
},
created () {
created() {
this.channelName()
this.loadFilter()
this.$store.dispatch('TimelineSpace/HeaderMenu/setupLoading')
},
watch: {
'$route': function () {
$route: function() {
this.channelName()
this.loadFilter()
}
},
methods: {
id () {
id() {
return this.$route.params.id
},
channelName () {
channelName() {
switch (this.$route.name) {
case 'home':
this.$store.commit('TimelineSpace/HeaderMenu/updateTitle', this.$t('header_menu.home'))
@ -139,15 +148,15 @@ export default {
break
}
},
switchStreaming () {
switchStreaming() {
this.$store.dispatch('TimelineSpace/stopStreamings')
this.$store.commit('TimelineSpace/changeUseWebsocket', !this.useWebsocket)
this.$store.dispatch('TimelineSpace/startStreamings')
},
openNewTootModal () {
openNewTootModal() {
this.$store.dispatch('TimelineSpace/Modals/NewToot/openModal')
},
reload () {
reload() {
switch (this.$route.name) {
case 'home':
case 'notifications':
@ -164,7 +173,7 @@ export default {
console.log('Not implemented')
}
},
reloadable () {
reloadable() {
switch (this.$route.name) {
case 'home':
case 'notifications':
@ -180,7 +189,7 @@ export default {
return false
}
},
loadFilter () {
loadFilter() {
switch (this.$route.name) {
case 'home':
this.filter = this.$store.state.TimelineSpace.Contents.Home.filter
@ -215,7 +224,7 @@ export default {
console.log('Not implemented')
}
},
applyFilter (filter) {
applyFilter(filter) {
switch (this.$route.name) {
case 'home':
this.$store.commit('TimelineSpace/Contents/Home/changeFilter', filter)
@ -251,7 +260,7 @@ export default {
}
this.filterVisible = false
},
filterable () {
filterable() {
switch (this.$route.name) {
case 'home':
case 'notifications':
@ -267,7 +276,7 @@ export default {
return false
}
},
extrasFilterable () {
extrasFilterable() {
switch (this.$route.name) {
case 'home':
return true
@ -275,7 +284,7 @@ export default {
return false
}
},
settings () {
settings() {
const url = `/${this.id()}/settings`
this.$router.push(url)
}
@ -303,6 +312,13 @@ export default {
.tools {
font-size: 18px;
display: flex;
justify-content: flex-end;
align-items: center;
.header-loading {
width: 18px;
}
.action {
color: var(--theme-secondary-color);

View File

@ -1,20 +1,24 @@
import Mastodon, { List, Response } from 'megalodon'
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '@/store'
import AxiosLoading from '@/utils/axiosLoading'
export interface HeaderMenuState {
title: string,
title: string
reload: boolean
loading: boolean
}
const state = (): HeaderMenuState => ({
title: 'Home',
reload: false
reload: false,
loading: false
})
export const MUTATION_TYPES = {
UPDATE_TITLE: 'updateTitle',
CHANGE_RELOAD: 'changeReload'
CHANGE_RELOAD: 'changeReload',
CHANGE_LOADING: 'changeLoading'
}
const mutations: MutationTree<HeaderMenuState> = {
@ -23,18 +27,27 @@ const mutations: MutationTree<HeaderMenuState> = {
},
[MUTATION_TYPES.CHANGE_RELOAD]: (state, value: boolean) => {
state.reload = value
},
[MUTATION_TYPES.CHANGE_LOADING]: (state, value: boolean) => {
state.loading = value
}
}
const actions: ActionTree<HeaderMenuState, RootState> = {
fetchList: async ({ commit, rootState }, listID: number): Promise<List> => {
const client = new Mastodon(
rootState.TimelineSpace.account.accessToken!,
rootState.TimelineSpace.account.baseURL + '/api/v1'
)
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<List> = await client.get<List>(`/lists/${listID}`)
commit(MUTATION_TYPES.UPDATE_TITLE, `#${res.data.title}`)
return res.data
},
setupLoading: ({ commit }) => {
const axiosLoading = new AxiosLoading()
axiosLoading.on('start', (_: number) => {
commit(MUTATION_TYPES.CHANGE_LOADING, true)
})
axiosLoading.on('done', () => {
commit(MUTATION_TYPES.CHANGE_LOADING, false)
})
}
}

View File

@ -0,0 +1,39 @@
import axios, { AxiosResponse } from 'axios'
import { EventEmitter } from 'events'
class AxiosLoading extends EventEmitter {
public requestCounter: number
constructor() {
super()
this.requestCounter = 0
this.setupRequest()
this.setupResponse()
}
private setupRequest() {
axios.interceptors.request.use(config => {
this.requestCounter++
this.emit('start', this.requestCounter)
return config
})
}
private setupResponse() {
const response = (response: AxiosResponse) => {
if (--this.requestCounter === 0) {
this.emit('done', {})
}
return response
}
const error = (error: any) => {
if (--this.requestCounter === 0) {
this.emit('done', {})
}
return Promise.reject(error)
}
axios.interceptors.response.use(response, error)
}
}
export default AxiosLoading