-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathuseApp.ts
More file actions
181 lines (166 loc) Β· 5.75 KB
/
Copy pathuseApp.ts
File metadata and controls
181 lines (166 loc) Β· 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import { useCallback, useEffect, useRef, useState } from 'react';
import { parseHash, pushHash } from '../services/deepLinkService';
import { logger } from '../services/logger';
import type { View } from '../types';
// QNBS-v3: All View values listed here β kept in sync with the View union in types.ts.
// Previously missing: analytics, zen, preview, progress; then the flag-gated views
// (objects/mindmap/characterInterviews/lora) β without these, a refresh/bookmark on a
// flag-gated view failed to restore and silently fell back to 'dashboard'.
const VALID_VIEWS = new Set<View>([
'dashboard',
'manuscript',
'writer',
'templates',
'outline',
'characters',
'world',
'export',
'settings',
'help',
'sceneboard',
'analytics',
'zen',
'characterGraph',
'consistencyChecker',
'critic',
'preview',
'progress',
'objects',
'mindmap',
'characterInterviews',
'lora',
// QNBS-v3: Scenario must survive deep links and persisted-view restoration.
'scenario',
]);
function isValidView(value: string): value is View {
return VALID_VIEWS.has(value as View);
}
function readInitialView(): View {
try {
// Hash-based deep links take priority over query params and localStorage.
const { view: hashView } = parseHash(window.location.hash);
if (hashView) return hashView;
} catch {
/* ignore */
}
try {
const fromUrl = new URLSearchParams(window.location.search).get('view');
if (fromUrl && isValidView(fromUrl)) return fromUrl;
} catch {
/* ignore */
}
try {
const stored = localStorage.getItem('worldscript-last-view');
if (stored && isValidView(stored)) return stored;
} catch {
/* ignore */
}
return 'dashboard';
}
// QNBS-v3: portal exit context distinguishes imported content from a project created in this app.
export interface PortalExitOptions {
allowInitialMetadataSeed?: boolean;
}
export const useApp = ({
isNewUser,
allowInitialMetadataSeed: initialSeedAuthority = isNewUser,
}: {
isNewUser: boolean;
allowInitialMetadataSeed?: boolean;
}) => {
const [currentView, setCurrentView] = useState<View>(() => readInitialView());
// QNBS-v3: remember the view navigated away from, so view-aware Help can open to the matching
// category (once inside Help, currentView is 'help' and no longer tells us where the user was).
const previousViewRef = useRef<View>('dashboard');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [allowInitialMetadataSeed, setAllowInitialMetadataSeed] = useState(initialSeedAuthority);
// QNBS-v3: initialize from boot project authority, with the legacy first-run fallback retained for direct hook consumers.
const [isPortalActive, setIsPortalActive] = useState(isNewUser);
const [isInitialLoad, setIsInitialLoad] = useState(true);
// QNBS-v3 (CodeAnt): the single place that mutates currentView, so previousView is tracked on
// EVERY navigation path β handleNavigate, hashchange (browser back/forward + deep links), and
// portal exit β not just sidebar clicks. Otherwise view-aware Help opens with a stale origin view.
const switchView = useCallback((view: View) => {
setCurrentView((prev) => {
if (prev !== view) previousViewRef.current = prev;
return view;
});
}, []);
useEffect(() => {
if (isNewUser) {
setIsPortalActive(true);
}
setIsInitialLoad(false);
}, [isNewUser]);
// QNBS-v3: Allow settings to re-open the welcome portal from any view.
useEffect(() => {
function onOpenPortal() {
setIsPortalActive(true);
}
window.addEventListener('worldscript:openPortal', onOpenPortal);
return () => window.removeEventListener('worldscript:openPortal', onOpenPortal);
}, []);
// QNBS-v3: web+worldscript protocol placeholder β manifest passes ?protocol= for future routing hooks.
useEffect(() => {
try {
const proto = new URLSearchParams(window.location.search).get('protocol');
if (proto) {
logger.debug('[DeepLink] protocol handler query reserved for future use');
}
} catch {
/* ignore */
}
}, []);
// QNBS-v3: Listen for hash changes so browser back/forward and external deep links work.
useEffect(() => {
function onHashChange() {
const { view } = parseHash(window.location.hash);
if (view && view !== currentView) {
switchView(view);
}
}
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, [currentView, switchView]);
// Save the current view to localStorage whenever it changes.
useEffect(() => {
try {
localStorage.setItem('worldscript-last-view', currentView);
} catch {
/* Storage unavailable */
}
}, [currentView]);
const handlePortalExit = useCallback(
(view?: View, options?: PortalExitOptions) => {
// QNBS-v3: imported/demo content revokes seed authority before bootstrap can treat it as fresh project data.
if (options?.allowInitialMetadataSeed === false) setAllowInitialMetadataSeed(false);
if (view) {
switchView(view);
pushHash(view);
}
setIsPortalActive(false);
},
[switchView],
);
// QNBS-v3: Keep URL hash in sync with navigation so all views are shareable/bookmarkable.
const handleNavigate = useCallback(
(view: View) => {
switchView(view);
pushHash(view);
},
[switchView],
);
return {
currentView,
previousView: previousViewRef.current,
isSidebarOpen,
isPortalActive,
isInitialLoad,
// QNBS-v3: expose transient boot/import authority without persisting a new project-state field.
allowInitialMetadataSeed,
handlePortalExit,
handleNavigate,
setIsSidebarOpen,
};
};
export type UseAppReturnType = ReturnType<typeof useApp>;