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**: 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))
- 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))

View File

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