sync: pull to refresh library (#271)

This commit is contained in:
Huang Xin
2025-01-30 01:47:42 +01:00
committed by GitHub
parent 27c6ed402e
commit fef75f123d
6 changed files with 154 additions and 5 deletions
@@ -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 };
};
+7 -3
View File
@@ -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();
+1 -1
View File
@@ -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;
+21
View File
@@ -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);
}