feat(timeline): swipe to refresh (close #547)

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/653/merge
Varun Patil 2023-11-01 13:19:48 -07:00
parent b1edd24dd9
commit 910cb4ada0
4 changed files with 118 additions and 7 deletions

View File

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
- **Feature**: RAW files are now hidden (stacked) when another file with the same basename exists ([#537](https://github.com/pulsejet/memories/issues/537), [#152](https://github.com/pulsejet/memories/issues/152), [#419](https://github.com/pulsejet/memories/issues/419)) - **Feature**: RAW files are now hidden (stacked) when another file with the same basename exists ([#537](https://github.com/pulsejet/memories/issues/537), [#152](https://github.com/pulsejet/memories/issues/152), [#419](https://github.com/pulsejet/memories/issues/419))
- **Feature**: Icon animation when playing live photos ([#898](https://github.com/pulsejet/memories/issues/898)) - **Feature**: Icon animation when playing live photos ([#898](https://github.com/pulsejet/memories/issues/898))
- **Feature**: Swipe to refresh on timeline ([#547](https://github.com/pulsejet/memories/issues/547))
- **Bugfix**: Allow switching video to direct on Safari ([#650](https://github.com/pulsejet/memories/issues/650)) - **Bugfix**: Allow switching video to direct on Safari ([#650](https://github.com/pulsejet/memories/issues/650))
- Many other [bug fixes](https://github.com/pulsejet/memories/milestone/18?closed=1) - Many other [bug fixes](https://github.com/pulsejet/memories/milestone/18?closed=1)
- Android app is now open source ([see](https://github.com/pulsejet/memories/tree/master/android)) - Android app is now open source ([see](https://github.com/pulsejet/memories/tree/master/android))

View File

@ -95,6 +95,7 @@ export default defineComponent({
emits: { emits: {
interactend: () => true, interactend: () => true,
scroll: (event: { current: number; previous: number }) => true,
}, },
data: () => ({ data: () => ({
@ -223,11 +224,13 @@ export default defineComponent({
const scroll = this.recycler?.$el?.scrollTop || 0; const scroll = this.recycler?.$el?.scrollTop || 0;
// Emit scroll event // Emit scroll event
utils.bus.emit('memories.recycler.scroll', { const event = {
current: scroll, current: scroll,
previous: this.lastKnownRecyclerScroll, previous: this.lastKnownRecyclerScroll,
dynTopMatterVisible: scroll < this.dynTopMatterHeight, dynTopMatterVisible: scroll < this.dynTopMatterHeight,
}); };
utils.bus.emit('memories.recycler.scroll', event);
this.$emit('scroll', event);
this.lastKnownRecyclerScroll = scroll; this.lastKnownRecyclerScroll = scroll;
// Get cursor px position // Get cursor px position

View File

@ -0,0 +1,97 @@
<template>
<div @touchstart.passive="touchstart" @touchmove.passive="touchmove" @touchend.passive="touchend">
<div v-if="on && progress" class="swipe-progress" :style="{ background: gradient }"></div>
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
const SWIPE_PX = 250;
export default defineComponent({
name: 'SwipeRefresh',
props: {
allowSwipe: {
type: Boolean,
default: true,
},
},
emits: {
refresh: () => true,
},
data: () => ({
on: false,
start: 0,
end: 0,
updateFrame: 0,
progress: 0,
}),
computed: {
gradient() {
const start = 50 - this.progress / 2;
const end = 50 + this.progress / 2;
const out = 'transparent';
const progress = 'var(--color-primary)';
return `linear-gradient(to right, ${out} ${start}%, ${progress} ${start}%, ${progress} ${end}%, ${out} ${end}%)`;
},
},
methods: {
/** Start gesture on container (passive) */
touchstart(event: TouchEvent) {
if (!this.allowSwipe) return;
const touch = event.touches[0];
this.end = this.start = touch.clientY;
this.progress = 0;
this.on = true;
},
/** Execute gesture on container (passive) */
touchmove(event: TouchEvent) {
if (!this.allowSwipe) return;
const touch = event.touches[0];
this.end = touch.clientY;
// Update progress only once per frame
this.updateFrame ||= requestAnimationFrame(() => {
this.updateFrame = 0;
// Compute percentage of swipe
const delta = (this.end - this.start) / SWIPE_PX;
this.progress = Math.min(Math.max(0, delta * 100), 100);
// Execute action on threshold
if (this.progress >= 100) {
this.on = false;
this.$emit('refresh');
}
});
},
/** End gesture on container (passive) */
touchend(event: TouchEvent) {
this.on = false;
},
},
});
</script>
<style lang="scss" scoped>
.swipe-progress {
position: absolute;
z-index: 100;
top: 0;
width: 100%;
height: 3px;
html.native & {
top: 2px;
}
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="container no-user-select" ref="container"> <SwipeRefresh class="container no-user-select" ref="container" :allowSwipe="allowSwipe" @refresh="softRefresh">
<!-- Loading indicator --> <!-- Loading indicator -->
<XLoadingIcon class="loading-icon centered" v-if="loading" /> <XLoadingIcon class="loading-icon centered" v-if="loading" />
@ -74,7 +74,8 @@
:fullHeight="scrollerHeight" :fullHeight="scrollerHeight"
:recycler="refs.recycler" :recycler="refs.recycler"
:recyclerBefore="refs.recyclerBefore" :recyclerBefore="refs.recyclerBefore"
@interactend="loadScrollView()" @interactend="loadScrollView"
@scroll="currentScroll = $event.current"
/> />
<SelectionManager <SelectionManager
@ -85,7 +86,7 @@
:recycler="refs.recycler?.$el" :recycler="refs.recycler?.$el"
@updateLoading="updateLoading" @updateLoading="updateLoading"
/> />
</div> </SwipeRefresh>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -103,6 +104,7 @@ import Photo from '@components/frame/Photo.vue';
import ScrollerManager from '@components/ScrollerManager.vue'; import ScrollerManager from '@components/ScrollerManager.vue';
import SelectionManager from '@components/SelectionManager.vue'; import SelectionManager from '@components/SelectionManager.vue';
import Viewer from '@components/viewer/Viewer.vue'; import Viewer from '@components/viewer/Viewer.vue';
import SwipeRefresh from './SwipeRefresh.vue';
import EmptyContent from '@components/top-matter/EmptyContent.vue'; import EmptyContent from '@components/top-matter/EmptyContent.vue';
import TopMatter from '@components/top-matter/TopMatter.vue'; import TopMatter from '@components/top-matter/TopMatter.vue';
@ -133,6 +135,7 @@ export default defineComponent({
SelectionManager, SelectionManager,
ScrollerManager, ScrollerManager,
Viewer, Viewer,
SwipeRefresh,
}, },
mixins: [UserConfig], mixins: [UserConfig],
@ -166,6 +169,8 @@ export default defineComponent({
currentStart: 0, currentStart: 0,
/** Current end index */ /** Current end index */
currentEnd: 0, currentEnd: 0,
/** Current physical scroll position */
currentScroll: 0,
/** Resizing timer */ /** Resizing timer */
resizeTimer: null as number | null, resizeTimer: null as number | null,
/** Height of the scroller */ /** Height of the scroller */
@ -219,7 +224,7 @@ export default defineComponent({
computed: { computed: {
refs() { refs() {
return this.$refs as { return this.$refs as {
container?: HTMLDivElement; container?: InstanceType<typeof SwipeRefresh>;
topmatter?: InstanceType<typeof TopMatter>; topmatter?: InstanceType<typeof TopMatter>;
dtm?: InstanceType<typeof DynamicTopMatter>; dtm?: InstanceType<typeof DynamicTopMatter>;
recycler?: VueRecyclerType; recycler?: VueRecyclerType;
@ -251,6 +256,11 @@ export default defineComponent({
showEmpty(): boolean { showEmpty(): boolean {
return !this.loading && this.empty; return !this.loading && this.empty;
}, },
/** Whether to allow swipe refresh */
allowSwipe(): boolean {
return !this.loading && this.currentScroll === 0;
},
}, },
methods: { methods: {
@ -384,7 +394,7 @@ export default defineComponent({
/** Recompute static sizes of containers */ /** Recompute static sizes of containers */
recomputeSizes() { recomputeSizes() {
// Size of outer container // Size of outer container
const e = this.refs.container!; const e = this.refs.container!.$el;
const height = e.clientHeight; const height = e.clientHeight;
const width = e.clientWidth; const width = e.clientWidth;
this.containerSize = [width, height]; this.containerSize = [width, height];