Loading src/App.vue +8 −1 Original line number Diff line number Diff line <template> <div :style="style"> <Refresh/> <NavBar/> <router-view :key="routerViewKey"></router-view> <ProviderData/> Loading @@ -12,6 +13,7 @@ import NavBar from './components/nav/NavBar' import { oneOf } from './util' import ProviderData from './components/nav/ProviderData' import Refresh from './components/generic/Refresh' function isTextField (field) { if (!field) { return false } Loading @@ -25,7 +27,12 @@ export default { name: 'App', components: { ProviderData, NavBar, Player }, components: { NavBar, Player, ProviderData, Refresh }, data () { return { hasFocus: isTextField(document.activeElement) Loading src/components/generic/Refresh.vue 0 → 100644 +78 −0 Original line number Diff line number Diff line <template> <div class="refresh" role="presentation"> <div class="refresh-indicator" :style="indicatorStyle" @animationend="animation = null"> <RefreshCwIcon/> </div> </div> </template> <script> import { RefreshCwIcon } from 'vue-feather-icons' import { pullRefreshController as refresh } from '../../util/dom' export default { components: { RefreshCwIcon }, data () { return { refresh, animation: null } }, computed: { indicatorStyle () { return { transition: !this.animation ? 'transform .05s' : null, transform: !this.animation ? `translateY(${-100 + refresh.triggerProgress * 1.75}%)` : null, animation: this.animation ? `${this.animation} 1s linear forwards` : null } } }, watch: { 'refresh.isRefreshing' (isRefreshing) { if (isRefreshing) { this.animation = 'refresh-spin' } } } } </script> <style lang="scss"> @import '../../styles/variables'; .refresh { display: flex; justify-content: center; position: fixed; top: 0; left: 0; right: 0; height: auto; pointer-events: none; z-index: 1000; } .refresh-indicator { background: $color-surface; width: 44px; height: 44px; color: white; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 0 4px rgba(0, 0, 0, .20); } @keyframes refresh-spin { 0% { transform: translateY(75%) rotate(0); } 50% { transform: translateY(25%) rotate(360deg); } 100% { transform: translateY(-100%) rotate(360deg); } } </style> src/util/dom.js +24 −8 Original line number Diff line number Diff line import { debounce } from 'throttle-debounce' import Vue from 'vue' export const pullRefreshController = (() => { let startY const TRIGGER_OFFSET_MAX = 150 let startY = null const refresh = debounce(350, false, () => { for (const subscriber of subscribers) { subscriber.refresh() } const notifySubscribers = debounce(350, false, () => { return Promise.all(subscribers.map(s => s.refresh())) .finally(() => { refresh.isRefreshing = false }) }) document.body.addEventListener('touchstart', e => { Loading @@ -15,13 +16,27 @@ export const pullRefreshController = (() => { document.body.addEventListener('touchmove', e => { const y = e.touches[0].pageY if (!startY || refresh.isRefreshing || subscribers.length === 0) return if (document.scrollingElement.scrollTop === 0 && y > startY) { refresh() const triggerOffset = y - startY refresh.triggerProgress = (triggerOffset / TRIGGER_OFFSET_MAX) * 100 if (triggerOffset > TRIGGER_OFFSET_MAX) { refresh.isRefreshing = true refresh.triggerProgress = 0 notifySubscribers() } } }, { passive: true }) document.body.addEventListener('touchend', e => { refresh.triggerProgress = 0 startY = null }, { passive: true }) const subscribers = [] return { const refresh = Vue.observable({ triggerProgress: 0, isRefreshing: false, subscribe (subscriber) { subscribers.push(subscriber) }, Loading @@ -31,5 +46,6 @@ export const pullRefreshController = (() => { subscribers.splice(index, 1) } } } }) return refresh })() Loading
src/App.vue +8 −1 Original line number Diff line number Diff line <template> <div :style="style"> <Refresh/> <NavBar/> <router-view :key="routerViewKey"></router-view> <ProviderData/> Loading @@ -12,6 +13,7 @@ import NavBar from './components/nav/NavBar' import { oneOf } from './util' import ProviderData from './components/nav/ProviderData' import Refresh from './components/generic/Refresh' function isTextField (field) { if (!field) { return false } Loading @@ -25,7 +27,12 @@ export default { name: 'App', components: { ProviderData, NavBar, Player }, components: { NavBar, Player, ProviderData, Refresh }, data () { return { hasFocus: isTextField(document.activeElement) Loading
src/components/generic/Refresh.vue 0 → 100644 +78 −0 Original line number Diff line number Diff line <template> <div class="refresh" role="presentation"> <div class="refresh-indicator" :style="indicatorStyle" @animationend="animation = null"> <RefreshCwIcon/> </div> </div> </template> <script> import { RefreshCwIcon } from 'vue-feather-icons' import { pullRefreshController as refresh } from '../../util/dom' export default { components: { RefreshCwIcon }, data () { return { refresh, animation: null } }, computed: { indicatorStyle () { return { transition: !this.animation ? 'transform .05s' : null, transform: !this.animation ? `translateY(${-100 + refresh.triggerProgress * 1.75}%)` : null, animation: this.animation ? `${this.animation} 1s linear forwards` : null } } }, watch: { 'refresh.isRefreshing' (isRefreshing) { if (isRefreshing) { this.animation = 'refresh-spin' } } } } </script> <style lang="scss"> @import '../../styles/variables'; .refresh { display: flex; justify-content: center; position: fixed; top: 0; left: 0; right: 0; height: auto; pointer-events: none; z-index: 1000; } .refresh-indicator { background: $color-surface; width: 44px; height: 44px; color: white; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 0 4px rgba(0, 0, 0, .20); } @keyframes refresh-spin { 0% { transform: translateY(75%) rotate(0); } 50% { transform: translateY(25%) rotate(360deg); } 100% { transform: translateY(-100%) rotate(360deg); } } </style>
src/util/dom.js +24 −8 Original line number Diff line number Diff line import { debounce } from 'throttle-debounce' import Vue from 'vue' export const pullRefreshController = (() => { let startY const TRIGGER_OFFSET_MAX = 150 let startY = null const refresh = debounce(350, false, () => { for (const subscriber of subscribers) { subscriber.refresh() } const notifySubscribers = debounce(350, false, () => { return Promise.all(subscribers.map(s => s.refresh())) .finally(() => { refresh.isRefreshing = false }) }) document.body.addEventListener('touchstart', e => { Loading @@ -15,13 +16,27 @@ export const pullRefreshController = (() => { document.body.addEventListener('touchmove', e => { const y = e.touches[0].pageY if (!startY || refresh.isRefreshing || subscribers.length === 0) return if (document.scrollingElement.scrollTop === 0 && y > startY) { refresh() const triggerOffset = y - startY refresh.triggerProgress = (triggerOffset / TRIGGER_OFFSET_MAX) * 100 if (triggerOffset > TRIGGER_OFFSET_MAX) { refresh.isRefreshing = true refresh.triggerProgress = 0 notifySubscribers() } } }, { passive: true }) document.body.addEventListener('touchend', e => { refresh.triggerProgress = 0 startY = null }, { passive: true }) const subscribers = [] return { const refresh = Vue.observable({ triggerProgress: 0, isRefreshing: false, subscribe (subscriber) { subscribers.push(subscriber) }, Loading @@ -31,5 +46,6 @@ export const pullRefreshController = (() => { subscribers.splice(index, 1) } } } }) return refresh })()