fix(sync): improve token switching and auto-sync behavior
- Introduce `lastSuccessToken` state to track the last verified sync token - Update manual sync to overwrite local favorites when switching tokens, treating it as a login action rather than a merge - Prevent auto-sync from running when the current token does not match the last successful one to avoid unintended data uploads
This commit is contained in:
87
index.html
87
index.html
@@ -870,6 +870,7 @@
|
|||||||
const [lastSyncedFavorites, setLastSyncedFavorites] = useState(() => JSON.parse(localStorage.getItem('th_favorites_synced')) || []);
|
const [lastSyncedFavorites, setLastSyncedFavorites] = useState(() => JSON.parse(localStorage.getItem('th_favorites_synced')) || []);
|
||||||
const [quality, setQuality] = useState(() => localStorage.getItem('th_quality') || '320k');
|
const [quality, setQuality] = useState(() => localStorage.getItem('th_quality') || '320k');
|
||||||
const [syncToken, setSyncToken] = useState(() => localStorage.getItem('th_sync_token') || '');
|
const [syncToken, setSyncToken] = useState(() => localStorage.getItem('th_sync_token') || '');
|
||||||
|
const [lastSuccessToken, setLastSuccessToken] = useState(() => localStorage.getItem('th_last_success_token') || '');
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
const [showFullPlayer, setShowFullPlayer] = useState(false);
|
const [showFullPlayer, setShowFullPlayer] = useState(false);
|
||||||
@@ -902,22 +903,25 @@
|
|||||||
useEffect(() => { localStorage.setItem('th_favorites_synced', JSON.stringify(lastSyncedFavorites)); }, [lastSyncedFavorites]);
|
useEffect(() => { localStorage.setItem('th_favorites_synced', JSON.stringify(lastSyncedFavorites)); }, [lastSyncedFavorites]);
|
||||||
useEffect(() => { localStorage.setItem('th_quality', quality); }, [quality]);
|
useEffect(() => { localStorage.setItem('th_quality', quality); }, [quality]);
|
||||||
useEffect(() => { localStorage.setItem('th_sync_token', syncToken); }, [syncToken]);
|
useEffect(() => { localStorage.setItem('th_sync_token', syncToken); }, [syncToken]);
|
||||||
|
useEffect(() => { localStorage.setItem('th_last_success_token', lastSuccessToken); }, [lastSuccessToken]);
|
||||||
|
|
||||||
// Auto Sync Logic
|
// Auto Sync Logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only auto-sync if we have a token and there are actual changes compared to last sync
|
// Only auto-sync if we have a token, it matches the last successfully synced token,
|
||||||
if (syncToken && JSON.stringify(favorites) !== JSON.stringify(lastSyncedFavorites)) {
|
// and there are actual changes compared to last sync.
|
||||||
|
// This prevents auto-syncing current favorites to a new token while typing/switching.
|
||||||
|
if (syncToken && syncToken === lastSuccessToken && JSON.stringify(favorites) !== JSON.stringify(lastSyncedFavorites)) {
|
||||||
// Use a small timeout to avoid rapid-fire syncs if user clicks quickly
|
// Use a small timeout to avoid rapid-fire syncs if user clicks quickly
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
autoSyncFavorites();
|
autoSyncFavorites();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [favorites, syncToken, lastSyncedFavorites]);
|
}, [favorites, syncToken, lastSuccessToken, lastSyncedFavorites]);
|
||||||
|
|
||||||
// 1. Auto Sync: Incremental update based on diff
|
// 1. Auto Sync: Incremental update based on diff
|
||||||
const autoSyncFavorites = async () => {
|
const autoSyncFavorites = async () => {
|
||||||
if (!syncToken) return;
|
if (!syncToken || syncToken !== lastSuccessToken) return;
|
||||||
try {
|
try {
|
||||||
// Get latest remote state
|
// Get latest remote state
|
||||||
const remoteFavorites = await syncService.get('favorites', syncToken) || [];
|
const remoteFavorites = await syncService.get('favorites', syncToken) || [];
|
||||||
@@ -957,36 +961,59 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Manual Sync: Merge (Union)
|
// 2. Manual Sync
|
||||||
// Used when user clicks "Sync" button or initial load
|
|
||||||
const manualSyncFavorites = async () => {
|
const manualSyncFavorites = async () => {
|
||||||
if (!syncToken) return;
|
if (!syncToken) return;
|
||||||
|
|
||||||
// Pull Remote
|
// Determine Mode: Switch (Overwrite Local) or Sync (Merge)
|
||||||
const cloudFavorites = await syncService.get('favorites', syncToken);
|
const isSwitchingToken = syncToken !== lastSuccessToken;
|
||||||
|
|
||||||
if (cloudFavorites && Array.isArray(cloudFavorites)) {
|
if (isSwitchingToken) {
|
||||||
// Merge Strategy: Union (Local + Remote)
|
// Mode: Switch Account/Token -> Remote Overwrites Local
|
||||||
// Since this is a manual sync (often initial), we assume user wants to keep everything
|
try {
|
||||||
const merged = [...favorites];
|
const cloudFavorites = await syncService.get('favorites', syncToken);
|
||||||
cloudFavorites.forEach(cloudSong => {
|
|
||||||
if (!merged.find(s => s.id === cloudSong.id)) {
|
// If cloud has data, use it. If null/empty, we assume new empty account.
|
||||||
merged.push(cloudSong);
|
const newFavorites = Array.isArray(cloudFavorites) ? cloudFavorites : [];
|
||||||
}
|
|
||||||
});
|
setFavorites(newFavorites);
|
||||||
|
setLastSyncedFavorites(newFavorites);
|
||||||
// Update Local
|
setLastSuccessToken(syncToken);
|
||||||
setFavorites(merged);
|
|
||||||
setLastSyncedFavorites(merged);
|
} catch (e) {
|
||||||
|
console.error("Token switch sync failed", e);
|
||||||
// Push Merged back to server
|
throw e; // Let UI show error
|
||||||
await syncService.set('favorites', merged, syncToken);
|
|
||||||
} else {
|
|
||||||
// If remote is empty/null, push local
|
|
||||||
if (favorites.length > 0) {
|
|
||||||
await syncService.set('favorites', favorites, syncToken);
|
|
||||||
setLastSyncedFavorites(favorites);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Mode: Regular Sync -> Merge / Union
|
||||||
|
|
||||||
|
// Pull Remote
|
||||||
|
const cloudFavorites = await syncService.get('favorites', syncToken);
|
||||||
|
|
||||||
|
if (cloudFavorites && Array.isArray(cloudFavorites)) {
|
||||||
|
// Merge: Union
|
||||||
|
const merged = [...favorites];
|
||||||
|
cloudFavorites.forEach(cloudSong => {
|
||||||
|
if (!merged.find(s => s.id === cloudSong.id)) {
|
||||||
|
merged.push(cloudSong);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Local
|
||||||
|
setFavorites(merged);
|
||||||
|
setLastSyncedFavorites(merged);
|
||||||
|
|
||||||
|
// Push Merged back to server
|
||||||
|
await syncService.set('favorites', merged, syncToken);
|
||||||
|
} else {
|
||||||
|
// Remote is empty, push local to it
|
||||||
|
if (favorites.length > 0) {
|
||||||
|
await syncService.set('favorites', favorites, syncToken);
|
||||||
|
setLastSyncedFavorites(favorites);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure token is marked as success
|
||||||
|
setLastSuccessToken(syncToken);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user