sync: pull to refresh library (#271)
This commit is contained in:
@@ -13,12 +13,17 @@ export const useBooksSync = () => {
|
||||
const { syncedBooks, syncBooks, lastSyncedAtBooks } = useSync();
|
||||
const syncBooksPullingRef = useRef(false);
|
||||
|
||||
const pullLibrary = async () => {
|
||||
if (!user) return;
|
||||
syncBooks([], 'pull');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
if (syncBooksPullingRef.current) return;
|
||||
syncBooksPullingRef.current = true;
|
||||
|
||||
syncBooks([], 'pull');
|
||||
pullLibrary();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -98,4 +103,6 @@ export const useBooksSync = () => {
|
||||
updateLibrary();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [syncedBooks]);
|
||||
|
||||
return { pullLibrary };
|
||||
};
|
||||
|
||||
@@ -21,14 +21,15 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
|
||||
import { useDemoBooks } from './hooks/useDemoBooks';
|
||||
import { useBooksSync } from './hooks/useBooksSync';
|
||||
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from './components/LibraryHeader';
|
||||
import Bookshelf from './components/Bookshelf';
|
||||
import { useBooksSync } from './hooks/useBooksSync';
|
||||
|
||||
const LibraryPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -49,8 +50,11 @@ const LibraryPage = () => {
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
const demoBooks = useDemoBooks();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useBooksSync();
|
||||
const { pullLibrary } = useBooksSync();
|
||||
|
||||
usePullToRefresh(containerRef, pullLibrary);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-200');
|
||||
@@ -310,7 +314,7 @@ const LibraryPage = () => {
|
||||
)}
|
||||
{libraryLoaded &&
|
||||
(libraryBooks.length > 0 ? (
|
||||
<div className='mt-12 flex-grow overflow-auto px-2'>
|
||||
<div ref={containerRef} className='mt-12 flex-grow overflow-auto px-2'>
|
||||
<Suspense>
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const TRIGGER_THRESHOLD = 120;
|
||||
const SHOW_INDICATOR_THRESHOLD = 60;
|
||||
|
||||
const MAX = 128;
|
||||
const k = 0.4;
|
||||
function appr(x: number) {
|
||||
return MAX * (1 - Math.exp((-k * x) / MAX));
|
||||
}
|
||||
|
||||
export const usePullToRefresh = (ref: React.RefObject<HTMLDivElement>, onTrigger: () => void) => {
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
el.addEventListener('touchstart', handleTouchStart);
|
||||
|
||||
function handleTouchStart(startEvent: TouchEvent) {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const initialY = startEvent.touches[0]!.clientY;
|
||||
|
||||
el.addEventListener('touchmove', handleTouchMove);
|
||||
el.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
function handleTouchMove(moveEvent: TouchEvent) {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const currentY = moveEvent.touches[0]!.clientY;
|
||||
const dy = currentY - initialY;
|
||||
|
||||
if (dy < 0) return;
|
||||
const parentEl = el.parentNode as HTMLDivElement;
|
||||
if (dy > TRIGGER_THRESHOLD) {
|
||||
flipArrow(parentEl);
|
||||
} else if (dy > SHOW_INDICATOR_THRESHOLD) {
|
||||
addPullIndicator(parentEl);
|
||||
} else {
|
||||
removePullIndicator(parentEl);
|
||||
}
|
||||
|
||||
el.style.transform = `translateY(${appr(dy)}px)`;
|
||||
}
|
||||
|
||||
function addPullIndicator(el: HTMLDivElement) {
|
||||
const indicator = el.querySelector('.pull-indicator');
|
||||
if (indicator) {
|
||||
if (indicator.classList.contains('flip')) {
|
||||
indicator.classList.remove('flip');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const pullIndicator = document.createElement('div');
|
||||
pullIndicator.className = 'pull-indicator text-gray-500';
|
||||
pullIndicator.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 19c-.3 0-.6-.1-.8-.3l-6-6c-.4-.4-.4-1 0-1.4s1-.4 1.4 0L11 16.2V5c0-.6.4-1 1-1s1 .4 1 1v11.2l4.4-4.4c.4-.4 1-.4 1.4 0s.4 1 0 1.4l-6 6c-.2.2-.5.3-.8.3z"/>
|
||||
</svg>
|
||||
`;
|
||||
el.appendChild(pullIndicator);
|
||||
}
|
||||
|
||||
function removePullIndicator(el: HTMLDivElement) {
|
||||
const pullIndicator = el.querySelector('.pull-indicator');
|
||||
if (pullIndicator) {
|
||||
pullIndicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function flipArrow(el: HTMLDivElement) {
|
||||
const pullIndicator = el.querySelector('.pull-indicator');
|
||||
if (pullIndicator && !pullIndicator.classList.contains('flip')) {
|
||||
pullIndicator.classList.add('flip');
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd(endEvent: TouchEvent) {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
el.style.transform = 'translateY(0)';
|
||||
removePullIndicator(el.parentNode as HTMLDivElement);
|
||||
|
||||
el.style.transition = 'transform 0.2s';
|
||||
|
||||
const y = endEvent.changedTouches[0]!.clientY;
|
||||
const dy = y - initialY;
|
||||
if (dy > TRIGGER_THRESHOLD) {
|
||||
onTrigger();
|
||||
}
|
||||
|
||||
el.addEventListener('transitionend', onTransitionEnd);
|
||||
|
||||
el.removeEventListener('touchmove', handleTouchMove);
|
||||
el.removeEventListener('touchend', handleTouchEnd);
|
||||
}
|
||||
|
||||
function onTransitionEnd() {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
el.style.transition = '';
|
||||
el.removeEventListener('transitionend', onTransitionEnd);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('touchstart', handleTouchStart);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref.current]);
|
||||
};
|
||||
@@ -225,6 +225,7 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
}
|
||||
if (uploaded) {
|
||||
book.deletedAt = null;
|
||||
book.updatedAt = Date.now();
|
||||
book.uploadedAt = Date.now();
|
||||
book.downloadedAt = Date.now();
|
||||
|
||||
@@ -371,7 +371,7 @@ export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web'
|
||||
export const READEST_WEB_BASE_URL = 'https://web.readest.com';
|
||||
|
||||
export const SYNC_PROGRESS_INTERVAL_SEC = 60;
|
||||
export const SYNC_NOTES_INTERVAL_SEC = 60;
|
||||
export const SYNC_NOTES_INTERVAL_SEC = 10;
|
||||
export const SYNC_BOOKS_INTERVAL_SEC = 10;
|
||||
export const CHECK_UPDATE_INTERVAL_SEC = 24 * 60 * 60;
|
||||
|
||||
|
||||
@@ -209,3 +209,24 @@ foliate-view {
|
||||
.scroll-container.hidden-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-lighter);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.pull-indicator svg {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.pull-indicator.flip svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user