Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
600949e475 | ||
|
|
26d2d3e379 | ||
|
|
61c2b62141 | ||
|
|
882296b1c0 | ||
|
|
4f8b0021c6 |
@@ -9,3 +9,5 @@ coverage/
|
|||||||
!.env.example
|
!.env.example
|
||||||
_sample-archives
|
_sample-archives
|
||||||
_gemini-plans
|
_gemini-plans
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ InstaArchive Viewer is a React 19 + Vite 6 PWA for browsing archived Instagram d
|
|||||||
- `npm run lint` — type-check only (`tsc --noEmit`)
|
- `npm run lint` — type-check only (`tsc --noEmit`)
|
||||||
- `npm test` / `npm run test:watch` — vitest
|
- `npm test` / `npm run test:watch` — vitest
|
||||||
- `npx vitest run src/lib/archive-patterns.test.ts` — a single test file
|
- `npx vitest run src/lib/archive-patterns.test.ts` — a single test file
|
||||||
- `npm run jd2 -- --archives <dir> --dry-run` — generate JDownloader `.crawljob`
|
|
||||||
files for every profile on disk (see `scripts/jd2-sync.ts` and
|
|
||||||
`docs/jdownloader.md`)
|
|
||||||
|
|
||||||
Local development usually needs both `npm run dev` and `npm run server`. Local-folder mode works without the backend; server-mode archives do not.
|
Local development usually needs both `npm run dev` and `npm run server`. Local-folder mode works without the backend; server-mode archives do not.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "instaarchive-viewer",
|
"name": "instaarchive-viewer",
|
||||||
"version": "1.8.0",
|
"version": "1.8.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "instaarchive-viewer",
|
"name": "instaarchive-viewer",
|
||||||
"version": "1.8.0",
|
"version": "1.8.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
|||||||
+3
-4
@@ -1,19 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "instaarchive-viewer",
|
"name": "instaarchive-viewer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.0",
|
"version": "1.8.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||||
"build": "vite build && npm run build:server",
|
"build": "vite build && npm run build:server",
|
||||||
"build:server": "tsc server.ts --esModuleInterop --module ESNext --target ES2022 --moduleResolution bundler --outDir dist-server",
|
"build:server": "tsc server.ts --esModuleInterop --module ESNext --target ES2022 --moduleResolution bundler --removeComments --outDir dist-server",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"server": "tsx server.ts",
|
"server": "tsx server.ts",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"lint": "tsc --noEmit",
|
"lint": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest"
|
||||||
"jd2": "tsx scripts/jd2-sync.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
|||||||
+56
-2
@@ -73,6 +73,17 @@ export default function App() {
|
|||||||
*/
|
*/
|
||||||
const initialRouteRef = useRef(parseRoute(window.location.pathname, window.location.search));
|
const initialRouteRef = useRef(parseRoute(window.location.pathname, window.location.search));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Back-button support for the post view. Every other URL change
|
||||||
|
* (`replaceState`s the tab/archive) is intentionally NOT pushed — only
|
||||||
|
* opening a post gets its own history entry, matching Instagram's own
|
||||||
|
* back-button behaviour: Back closes the post instead of leaving the app.
|
||||||
|
*/
|
||||||
|
const pushedPostRef = useRef(false);
|
||||||
|
/** Set while reacting to a popstate, so the URL-sync effect below does not
|
||||||
|
* try to push/replace/back() again for a change the browser already made. */
|
||||||
|
const suppressNextSyncRef = useRef(false);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const profilePicInputRef = useRef<HTMLInputElement>(null);
|
const profilePicInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -367,6 +378,13 @@ export default function App() {
|
|||||||
// loader below is waiting to read.
|
// loader below is waiting to read.
|
||||||
if (!hasInitialLoaded) return;
|
if (!hasInitialLoaded) return;
|
||||||
|
|
||||||
|
// Consumed exactly once per popstate, regardless of what happens below:
|
||||||
|
// a popstate-driven change often already matches the URL (the browser
|
||||||
|
// already moved the pointer), which used to leave this flag stuck true
|
||||||
|
// and silently no-op the NEXT real close/open until something reset it.
|
||||||
|
const wasPopState = suppressNextSyncRef.current;
|
||||||
|
suppressNextSyncRef.current = false;
|
||||||
|
|
||||||
const archive = currentArchive?.name ?? (allPosts.length > 0 ? username : null) ?? null;
|
const archive = currentArchive?.name ?? (allPosts.length > 0 ? username : null) ?? null;
|
||||||
const nextPath = buildPath({
|
const nextPath = buildPath({
|
||||||
archive,
|
archive,
|
||||||
@@ -374,12 +392,48 @@ export default function App() {
|
|||||||
post: selectedPost ? postSlug(selectedPost) : null,
|
post: selectedPost ? postSlug(selectedPost) : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextPath !== window.location.pathname + window.location.search) {
|
if (nextPath === window.location.pathname + window.location.search) return;
|
||||||
console.log(`[Permalink] Updating URL to: ${nextPath}`);
|
if (wasPopState) return; // the browser already navigated; nothing to add
|
||||||
|
|
||||||
|
console.log(`[Permalink] Updating URL to: ${nextPath}`);
|
||||||
|
|
||||||
|
if (selectedPost && !pushedPostRef.current) {
|
||||||
|
// Opening a post: push, so Back closes it instead of leaving the app.
|
||||||
|
window.history.pushState(null, '', nextPath);
|
||||||
|
pushedPostRef.current = true;
|
||||||
|
} else if (!selectedPost && pushedPostRef.current) {
|
||||||
|
// Closing a post that was pushed for: consume that entry rather than
|
||||||
|
// piling a new one on top of it, so Back still means "one step".
|
||||||
|
pushedPostRef.current = false;
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
window.history.replaceState(null, '', nextPath);
|
window.history.replaceState(null, '', nextPath);
|
||||||
}
|
}
|
||||||
}, [hasInitialLoaded, currentArchive?.name, username, allPosts.length, activeTab, selectedPost?.id]);
|
}, [hasInitialLoaded, currentArchive?.name, username, allPosts.length, activeTab, selectedPost?.id]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Back/forward support for the post view. Only a post push (above) ever
|
||||||
|
* creates an entry, so this only ever needs to open or close a post —
|
||||||
|
* never re-derive the tab or archive, which stayed on replaceState.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const onPopState = () => {
|
||||||
|
suppressNextSyncRef.current = true;
|
||||||
|
const route = parseRoute(window.location.pathname, window.location.search);
|
||||||
|
const post = route.post ? findPostBySlug(allPosts, route.post) : null;
|
||||||
|
if (post) {
|
||||||
|
setActiveTab(tabForSource(post.source));
|
||||||
|
setSelectedPost(post);
|
||||||
|
pushedPostRef.current = true; // forward navigation can land back here
|
||||||
|
} else {
|
||||||
|
setSelectedPost(null);
|
||||||
|
pushedPostRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('popstate', onPopState);
|
||||||
|
return () => window.removeEventListener('popstate', onPopState);
|
||||||
|
}, [allPosts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasInitialLoaded) return;
|
if (hasInitialLoaded) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user