Skip to content

Commit b6ec2a0

Browse files
committed
feat: implement application lifecycle and window management
Signed-off-by: Innei <[email protected]>
1 parent 61d5611 commit b6ec2a0

23 files changed

+526
-969
lines changed

apps/desktop/layer/main/export.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Export types for renderer to use
22
export type { IpcServices } from "./src/ipc"
3-
export type { RendererHandlers } from "./src/renderer-handlers"
43

54
// Export services for potential main process use
65
export { services } from "./src/ipc"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import path from "node:path"
2+
3+
import { app, protocol } from "electron"
4+
5+
import { initializeSentry } from "./sentry"
6+
7+
if (import.meta.env.DEV) app.setPath("userData", path.join(app.getPath("appData"), "Folo(dev)"))
8+
protocol.registerSchemesAsPrivileged([
9+
{
10+
scheme: "sentry-ipc",
11+
privileges: { bypassCSP: true, corsEnabled: true, supportFetchAPI: true, secure: true },
12+
},
13+
{
14+
scheme: "app",
15+
privileges: {
16+
standard: true,
17+
bypassCSP: true,
18+
supportFetchAPI: true,
19+
secure: true,
20+
},
21+
},
22+
])
23+
// Solve Sentry SDK should be initialized before the Electron app 'ready' event is fired
24+
initializeSentry()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { app } from "electron"
2+
import squirrelStartup from "electron-squirrel-startup"
3+
4+
import { DEVICE_ID } from "./constants/system"
5+
import { BootstrapManager } from "./manager/bootstrap"
6+
7+
console.info("[main] device id:", DEVICE_ID)
8+
if (squirrelStartup) {
9+
app.quit()
10+
}
11+
12+
BootstrapManager.start()

apps/desktop/layer/main/src/index.ts

Lines changed: 2 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -1,222 +1,2 @@
1-
import "./side-effects"
2-
3-
import { electronApp, optimizer } from "@electron-toolkit/utils"
4-
import { callWindowExpose } from "@follow/shared/bridge"
5-
import { DEV, LEGACY_APP_PROTOCOL } from "@follow/shared/constants"
6-
import { env } from "@follow/shared/env.desktop"
7-
import { createBuildSafeHeaders } from "@follow/utils/headers"
8-
import { IMAGE_PROXY_URL } from "@follow/utils/img-proxy"
9-
import { parse } from "cookie-es"
10-
import { app, BrowserWindow, net, protocol, session } from "electron"
11-
import squirrelStartup from "electron-squirrel-startup"
12-
13-
import { DEVICE_ID } from "./constants/system"
14-
import { isMacOS } from "./env"
15-
import { initializeAppStage0, initializeAppStage1 } from "./init"
16-
import { updateProxy } from "./lib/proxy"
17-
import { handleUrlRouting } from "./lib/router"
18-
import { store } from "./lib/store"
19-
import { registerAppTray } from "./lib/tray"
20-
import { updateNotificationsToken } from "./lib/user"
21-
import { logger } from "./logger"
22-
import { registerUpdater } from "./updater"
23-
import { cleanupOldRender } from "./updater/hot-updater"
24-
import { WindowManager } from "./window"
25-
26-
if (DEV) console.info("[main] env loaded:", env)
27-
28-
const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
29-
30-
console.info("[main] device id:", DEVICE_ID)
31-
if (squirrelStartup) {
32-
app.quit()
33-
}
34-
35-
const buildSafeHeaders = createBuildSafeHeaders(env.VITE_WEB_URL, [
36-
env.VITE_OPENPANEL_API_URL || "",
37-
IMAGE_PROXY_URL,
38-
env.VITE_API_URL,
39-
// Fix unexpected CORS error when modify the origin header to request domain in the preflight request
40-
// Learn more https://github.com/RSSNext/Folo/issues/3312
41-
"https://readwise.io",
42-
])
43-
44-
function bootstrap() {
45-
initializeAppStage0()
46-
const gotTheLock = app.requestSingleInstanceLock()
47-
48-
if (!gotTheLock) {
49-
app.quit()
50-
51-
return
52-
}
53-
54-
let mainWindow: BrowserWindow
55-
56-
initializeAppStage1()
57-
58-
app.on("second-instance", (_, commandLine) => {
59-
if (mainWindow) {
60-
if (mainWindow.isMinimized()) mainWindow.restore()
61-
mainWindow.show()
62-
}
63-
64-
const url = commandLine.pop()
65-
if (url) {
66-
handleOpen(url)
67-
}
68-
})
69-
70-
app.on("activate", () => {
71-
// On macOS it's common to re-create a window in the app when the
72-
// dock icon is clicked and there are no other windows open.
73-
mainWindow = WindowManager.getMainWindowOrCreate()
74-
mainWindow.show()
75-
})
76-
77-
// This method will be called when Electron has finished
78-
// initialization and is ready to create browser windows.
79-
// Some APIs can only be used after this event occurs.
80-
app.whenReady().then(async () => {
81-
protocol.handle("app", (request) => {
82-
try {
83-
const urlObj = new URL(request.url)
84-
return net.fetch(`file://${urlObj.pathname}`)
85-
} catch {
86-
logger.error("app protocol error", request.url)
87-
return new Response("Not found", { status: 404 })
88-
}
89-
})
90-
91-
// Default open or close DevTools by F12 in development
92-
// and ignore CommandOrControl + R in production.
93-
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
94-
app.on("browser-window-created", (_, window) => {
95-
optimizer.watchWindowShortcuts(window)
96-
})
97-
98-
// Set app user model id for windows
99-
electronApp.setAppUserModelId(`re.${LEGACY_APP_PROTOCOL}`)
100-
101-
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
102-
details.requestHeaders = buildSafeHeaders({
103-
url: details.url,
104-
headers: details.requestHeaders,
105-
})
106-
107-
callback({ cancel: false, requestHeaders: details.requestHeaders })
108-
})
109-
110-
mainWindow = WindowManager.createMainWindow()
111-
112-
updateProxy()
113-
registerUpdater()
114-
registerAppTray()
115-
updateNotificationsToken()
116-
117-
app.on("open-url", (_, url) => {
118-
if (mainWindow && !mainWindow.isDestroyed()) {
119-
if (mainWindow.isMinimized()) mainWindow.restore()
120-
mainWindow.focus()
121-
} else {
122-
mainWindow = WindowManager.createMainWindow()
123-
}
124-
url && handleOpen(url)
125-
})
126-
127-
// for dev debug
128-
129-
if (process.env.NODE_ENV === "development") {
130-
import("electron-devtools-installer").then(
131-
({ default: installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS }) => {
132-
;[REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS].forEach((extension) => {
133-
installExtension(extension, {
134-
loadExtensionOptions: { allowFileAccess: true },
135-
})
136-
.then((name) => console.info(`Added Extension: ${name}`))
137-
.catch((err) => console.info("An error occurred:", err))
138-
})
139-
140-
session.defaultSession.getAllExtensions().forEach((e) => {
141-
session.defaultSession.loadExtension(e.path)
142-
})
143-
},
144-
)
145-
}
146-
})
147-
148-
app.on("before-quit", async () => {
149-
// store window pos when before app quit
150-
const window = WindowManager.getMainWindow()
151-
if (!window || window.isDestroyed()) return
152-
const bounds = window.getBounds()
153-
154-
store.set(WindowManager.windowStateStoreKey, {
155-
width: bounds.width,
156-
height: bounds.height,
157-
x: bounds.x,
158-
y: bounds.y,
159-
})
160-
await session.defaultSession.cookies.flushStore()
161-
162-
await cleanupOldRender()
163-
})
164-
165-
const handleOpen = async (url: string) => {
166-
const isValid = URL.canParse(url)
167-
if (!isValid) return
168-
const urlObj = new URL(url)
169-
170-
if (urlObj.hostname === "auth" || urlObj.pathname === "//auth") {
171-
const token = urlObj.searchParams.get("token")
172-
173-
if (token) {
174-
await callWindowExpose(mainWindow).applyOneTimeToken(token)
175-
} else {
176-
// compatible with old version of ssr, should be removed in 0.4.4
177-
const ck = urlObj.searchParams.get("ck")
178-
const userId = urlObj.searchParams.get("userId")
179-
180-
if (ck && apiURL) {
181-
const cookie = parse(atob(ck), { decode: (value) => value })
182-
Object.keys(cookie).forEach(async (name) => {
183-
const value = cookie[name]
184-
await mainWindow.webContents.session.cookies.set({
185-
url: apiURL,
186-
name,
187-
value,
188-
secure: true,
189-
httpOnly: true,
190-
domain: new URL(apiURL).hostname,
191-
sameSite: "no_restriction",
192-
expirationDate: new Date().setDate(new Date().getDate() + 30),
193-
})
194-
})
195-
196-
userId && (await callWindowExpose(mainWindow).clearIfLoginOtherAccount(userId))
197-
mainWindow.reload()
198-
199-
updateNotificationsToken()
200-
}
201-
}
202-
} else {
203-
handleUrlRouting(url)
204-
}
205-
}
206-
207-
// Quit when all windows are closed, except on macOS. There, it's common
208-
// for applications and their menu bar to stay active until the user quits
209-
// explicitly with Cmd + Q.
210-
app.on("window-all-closed", () => {
211-
if (!isMacOS) {
212-
app.quit()
213-
}
214-
})
215-
216-
app.on("before-quit", () => {
217-
const windows = BrowserWindow.getAllWindows()
218-
windows.forEach((window) => window.destroy())
219-
})
220-
}
221-
222-
bootstrap()
1+
import "./before-bootstrap"
2+
import "./bootstrap"

0 commit comments

Comments
 (0)