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**: 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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
<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];
|
||||||
|
|
Loading…
Reference in New Issue