feat(timeline): swipe to refresh (close #547)
Signed-off-by: Varun Patil <radialapps@gmail.com>pull/653/merge
parent
b1edd24dd9
commit
910cb4ada0
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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];
|
||||
|
|
Loading…
Reference in New Issue