feat: split main tipc routes and init posthog, tracker of update event (#153)

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-07-25 18:40:32 +08:00 committed by GitHub
parent 14b7eeaf5d
commit cdc8703869
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 315 additions and 247 deletions

16
src/main/posthog.ts Normal file
View File

@ -0,0 +1,16 @@
import { env } from "@env"
import Posthog from "posthog-js"
export const initPosthog = () => {
if (env.VITE_POSTHOG_KEY === undefined) return
Posthog.init(env.VITE_POSTHOG_KEY, {
person_profiles: "identified_only",
debug: true,
})
return Posthog
}
export const posthog = initPosthog()
export const trackEvent = (event: string, properties?: Record<string, any>) => {
posthog?.capture(event, properties)
}

View File

@ -1,236 +0,0 @@
import { createRequire } from "node:module"
import { getRendererHandlers, tipc } from "@egoist/tipc/main"
import { callGlobalContextMethod } from "@shared/bridge"
import type { BrowserWindow, MessageBoxOptions } from "electron"
import { app, dialog, Menu, nativeTheme, ShareMenu } from "electron"
import { downloadFile } from "./lib/download"
import type { RendererHandlers } from "./renderer-handlers"
import { quitAndInstall } from "./updater"
import { createSettingWindow, getMainWindow } from "./window"
const t = tipc.create()
const require = createRequire(import.meta.url)
export const router = {
inspectElement: t.procedure
.input<{ x: number, y: number }>()
.action(async ({ input, context }) => {
context.sender.inspectElement(input.x, input.y)
}),
showContextMenu: t.procedure
.input<{
items: Array<
| {
type: "text"
label: string
enabled?: boolean
shortcut?: string
icon?: string
}
| { type: "separator" }
>
}>()
.action(async ({ input, context }) => {
const menu = Menu.buildFromTemplate(
input.items.map((item, index) => {
if (item.type === "separator") {
return {
type: "separator" as const,
}
}
return {
label: item.label,
enabled: item.enabled ?? true,
accelerator: item.shortcut,
click() {
context.sender.send("menu-click", index)
},
}
}),
)
menu.popup({
callback: () => {
context.sender.send("menu-closed")
},
})
}),
showConfirmDialog: t.procedure
.input<{
title: string
message: string
options?: Partial<MessageBoxOptions>
}>()
.action(async ({ input }) => {
const result = await dialog.showMessageBox({
message: input.title,
detail: input.message,
buttons: ["Confirm", "Cancel"],
...input.options,
})
return result.response === 0
}),
showShareMenu: t.procedure
.input<string>()
.action(async ({ input, context }) => {
const menu = new ShareMenu({
urls: [input],
})
menu.popup({
callback: () => {
context.sender.send("menu-closed")
},
})
}),
saveToEagle: t.procedure
.input<{ url: string, mediaUrls: string[] }>()
.action(async ({ input }) => {
try {
const res = await fetch("http://localhost:41595/api/item/addFromURLs", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
items: input.mediaUrls?.map((media) => ({
url: media,
website: input.url,
headers: {
referer: input.url,
},
})),
}),
})
return await res.json()
} catch {
return null
}
}),
invalidateQuery: t.procedure
.input<(string | number | undefined)[]>()
.action(async ({ input }) => {
const mainWindow = getMainWindow()
if (!mainWindow) return
const handlers = getRendererHandlers<RendererHandlers>(
mainWindow.webContents,
)
handlers.invalidateQuery.send(input)
}),
getLoginItemSettings: t.procedure
.input<void>()
.action(async () => await app.getLoginItemSettings()),
setLoginItemSettings: t.procedure
.input<boolean>()
.action(async ({ input }) => {
app.setLoginItemSettings({
openAtLogin: input,
})
}),
openSettingWindow: t.procedure.action(async () => createSettingWindow()),
getSystemFonts: t.procedure.action(
async (): Promise<string[]> =>
new Promise((resolve) => {
// NOTE: should external font-list deps
// use `require` to avoid bundling, vite behavior
require("font-list")
.getFonts()
.then((fonts) => {
resolve(fonts.map((font) => font.replaceAll("\"", "")))
})
}),
),
setAppearance: t.procedure
.input<"light" | "dark" | "system">()
.action(async ({ input }) => {
// NOTE: Temporarily changing to system to get the color mode that system is in at the moment may cause a bit of a problem.
// On macos, there is a bug, traffic lights flicker
nativeTheme.themeSource = "system"
const systemColorMode = nativeTheme.shouldUseDarkColors ?
"dark" :
"light"
nativeTheme.themeSource = systemColorMode === input ? "system" : input
}),
setDockBadge: t.procedure.input<number>().action(async ({ input }) => {
if (app.dock) {
if (input === 0) {
app.dock.setBadge("")
} else {
app.dock.setBadge(input.toString())
}
} else {
app.setBadgeCount(input)
}
}),
saveImage: t.procedure.input<string>().action(async ({ input }) => {
const result = await dialog.showSaveDialog({
defaultPath: input.split("/").pop(),
})
if (result.canceled) return
// return result.filePath;
await downloadFile(input, result.filePath).catch((err) => {
const mainWindow = getMainWindow()
if (!mainWindow) return
callGlobalContextMethod(mainWindow, "toast.error", ["Download failed!"])
throw err
})
const mainWindow = getMainWindow()
if (!mainWindow) return
callGlobalContextMethod(mainWindow, "toast.success", ["Download success!"])
}),
windowAction: t.procedure
.input<{ action: "close" | "minimize" | "maximum" }>()
.action(async ({ input, context }) => {
if (context.sender.getType() === "window") {
const window: BrowserWindow | null = (
context.sender as Sender
).getOwnerBrowserWindow()
if (!window) return
switch (input.action) {
case "close": {
window.close()
break
}
case "minimize": {
window.minimize()
break
}
case "maximum": {
if (window.isMaximized()) {
window.unmaximize()
} else {
window.maximize()
}
break
}
}
}
}),
quitAndInstall: t.procedure.action(async () => {
quitAndInstall()
}),
}
interface Sender extends Electron.WebContents {
getOwnerBrowserWindow: () => Electron.BrowserWindow | null
}
export type Router = typeof router

View File

@ -0,0 +1,5 @@
import { tipc } from "@egoist/tipc/main"
export const t = tipc.create()
export type T = typeof t

82
src/main/tipc/app.ts Normal file
View File

@ -0,0 +1,82 @@
import { getRendererHandlers } from "@egoist/tipc/main"
import type { BrowserWindow } from "electron"
import type { RendererHandlers } from "../renderer-handlers"
import { quitAndInstall } from "../updater"
import { getMainWindow } from "../window"
import { t } from "./_instance"
export const appRoute = {
saveToEagle: t.procedure
.input<{ url: string, mediaUrls: string[] }>()
.action(async ({ input }) => {
try {
const res = await fetch("http://localhost:41595/api/item/addFromURLs", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
items: input.mediaUrls?.map((media) => ({
url: media,
website: input.url,
headers: {
referer: input.url,
},
})),
}),
})
return await res.json()
} catch {
return null
}
}),
invalidateQuery: t.procedure
.input<(string | number | undefined)[]>()
.action(async ({ input }) => {
const mainWindow = getMainWindow()
if (!mainWindow) return
const handlers = getRendererHandlers<RendererHandlers>(
mainWindow.webContents,
)
handlers.invalidateQuery.send(input)
}),
windowAction: t.procedure
.input<{ action: "close" | "minimize" | "maximum" }>()
.action(async ({ input, context }) => {
if (context.sender.getType() === "window") {
const window: BrowserWindow | null = (
context.sender as Sender
).getOwnerBrowserWindow()
if (!window) return
switch (input.action) {
case "close": {
window.close()
break
}
case "minimize": {
window.minimize()
break
}
case "maximum": {
if (window.isMaximized()) {
window.unmaximize()
} else {
window.maximize()
}
break
}
}
}
}),
quitAndInstall: t.procedure.action(async () => {
quitAndInstall()
}),
}
interface Sender extends Electron.WebContents {
getOwnerBrowserWindow: () => Electron.BrowserWindow | null
}

9
src/main/tipc/debug.ts Normal file
View File

@ -0,0 +1,9 @@
import { t } from "./_instance"
export const debugRoute = {
inspectElement: t.procedure
.input<{ x: number, y: number }>()
.action(async ({ input, context }) => {
context.sender.inspectElement(input.x, input.y)
}),
}

15
src/main/tipc/index.ts Normal file
View File

@ -0,0 +1,15 @@
import { appRoute } from "./app"
import { debugRoute } from "./debug"
import { menuRoute } from "./menu"
import { settingRoute } from "./setting"
import { trackerRoute } from "./tracker"
export const router = {
...debugRoute,
...menuRoute,
...settingRoute,
...appRoute,
...trackerRoute,
}
export type Router = typeof router

99
src/main/tipc/menu.ts Normal file
View File

@ -0,0 +1,99 @@
import { callGlobalContextMethod } from "@shared/bridge"
import type { MessageBoxOptions } from "electron"
import { dialog, Menu, ShareMenu } from "electron"
import { downloadFile } from "../lib/download"
import { getMainWindow } from "../window"
import { t } from "./_instance"
export const menuRoute = {
showContextMenu: t.procedure
.input<{
items: Array<
| {
type: "text"
label: string
enabled?: boolean
shortcut?: string
icon?: string
}
| { type: "separator" }
>
}>()
.action(async ({ input, context }) => {
const menu = Menu.buildFromTemplate(
input.items.map((item, index) => {
if (item.type === "separator") {
return {
type: "separator" as const,
}
}
return {
label: item.label,
enabled: item.enabled ?? true,
accelerator: item.shortcut,
click() {
context.sender.send("menu-click", index)
},
}
}),
)
menu.popup({
callback: () => {
context.sender.send("menu-closed")
},
})
}),
/** @deprecated */
showConfirmDialog: t.procedure
.input<{
title: string
message: string
options?: Partial<MessageBoxOptions>
}>()
.action(async ({ input }) => {
const result = await dialog.showMessageBox({
message: input.title,
detail: input.message,
buttons: ["Confirm", "Cancel"],
...input.options,
})
return result.response === 0
}),
showShareMenu: t.procedure
.input<string>()
.action(async ({ input, context }) => {
const menu = new ShareMenu({
urls: [input],
})
menu.popup({
callback: () => {
context.sender.send("menu-closed")
},
})
}),
saveImage: t.procedure.input<string>().action(async ({ input }) => {
const result = await dialog.showSaveDialog({
defaultPath: input.split("/").pop(),
})
if (result.canceled) return
// return result.filePath;
await downloadFile(input, result.filePath).catch((err) => {
const mainWindow = getMainWindow()
if (!mainWindow) return
callGlobalContextMethod(mainWindow, "toast.error", ["Download failed!"])
throw err
})
const mainWindow = getMainWindow()
if (!mainWindow) return
callGlobalContextMethod(mainWindow, "toast.success", ["Download success!"])
}),
}

46
src/main/tipc/setting.ts Normal file
View File

@ -0,0 +1,46 @@
import { app, nativeTheme } from "electron"
import { createSettingWindow } from "../window"
import { t } from "./_instance"
export const settingRoute = { getLoginItemSettings: t.procedure
.input<void>()
.action(async () => await app.getLoginItemSettings()), setLoginItemSettings: t.procedure
.input<boolean>()
.action(async ({ input }) => {
app.setLoginItemSettings({
openAtLogin: input,
})
}), openSettingWindow: t.procedure.action(async () => createSettingWindow()), getSystemFonts: t.procedure.action(
async (): Promise<string[]> =>
new Promise((resolve) => {
// NOTE: should external font-list deps
// use `require` to avoid bundling, vite behavior
require("font-list")
.getFonts()
.then((fonts) => {
resolve(fonts.map((font) => font.replaceAll("\"", "")))
})
}),
), setAppearance: t.procedure
.input<"light" | "dark" | "system">()
.action(async ({ input }) => {
// NOTE: Temporarily changing to system to get the color mode that system is in at the moment may cause a bit of a problem.
// On macos, there is a bug, traffic lights flicker
nativeTheme.themeSource = "system"
const systemColorMode = nativeTheme.shouldUseDarkColors ?
"dark" :
"light"
nativeTheme.themeSource = systemColorMode === input ? "system" : input
}), setDockBadge: t.procedure.input<number>().action(async ({ input }) => {
if (app.dock) {
if (input === 0) {
app.dock.setBadge("")
} else {
app.dock.setBadge(input.toString())
}
} else {
app.setBadgeCount(input)
}
}) }

19
src/main/tipc/tracker.ts Normal file
View File

@ -0,0 +1,19 @@
import type { User } from "@auth/core/types"
import { posthog } from "../posthog"
import { t } from "./_instance"
export const trackerRoute = {
trackerIdentify: t.procedure
.input<{
user: User
}>()
.action(async ({ input }) => {
const { user } = input
posthog?.identify(user.id, {
name: user.name,
handle: user.handle,
})
}),
}

View File

@ -3,6 +3,7 @@ import { autoUpdater } from "electron-updater"
import { channel, isDev } from "../env"
import { logger } from "../logger"
import { trackEvent } from "../posthog"
import type { RendererHandlers } from "../renderer-handlers"
import { getMainWindow } from "../window"
import { CustomGitHubProvider } from "./custom-github-provider"
@ -67,16 +68,17 @@ export const registerUpdater = async () => {
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.autoRunAppAfterInstall = true
const feedUrl: Exclude<Parameters<typeof autoUpdater.setFeedURL>[0], string> = {
channel,
// hack for custom provider
provider: "custom" as "github",
repo: "follow",
owner: "RSSNext",
releaseType: channel === "stable" ? "release" : "prerelease",
// @ts-expect-error hack for custom provider
updateProvider: CustomGitHubProvider,
}
const feedUrl: Exclude<Parameters<typeof autoUpdater.setFeedURL>[0], string> =
{
channel,
// hack for custom provider
provider: "custom" as "github",
repo: "follow",
owner: "RSSNext",
releaseType: channel === "stable" ? "release" : "prerelease",
// @ts-expect-error hack for custom provider
updateProvider: CustomGitHubProvider,
}
logger.debug("auto-updater feed config", {
...feedUrl,
@ -103,7 +105,7 @@ export const registerUpdater = async () => {
autoUpdater.on("download-progress", (e) => {
logger.info(`Download progress: ${e.percent}`)
})
autoUpdater.on("update-downloaded", () => {
autoUpdater.on("update-downloaded", (e) => {
downloading = false
logger.info("Update downloaded, ready to install")
@ -112,6 +114,11 @@ export const registerUpdater = async () => {
const handlers = getRendererHandlers<RendererHandlers>(
mainWindow.webContents,
)
trackEvent("update_ready_to_install", {
version: e.version,
releaseVersion: e.releaseName,
})
handlers.updateDownloaded.send()
})
autoUpdater.on("error", (e) => {

View File

@ -17,6 +17,8 @@ export const AutoUpdater = () => {
const handleClick = useCallback(() => {
setUpdaterStatus(false)
window.posthog?.capture("update_restart")
tipcClient?.quitAndInstall()
}, [])

View File

@ -1,4 +1,5 @@
import { useSetMe } from "@renderer/atoms/user"
import { tipcClient } from "@renderer/lib/client"
import { useSession } from "@renderer/queries/auth"
import { CleanerService } from "@renderer/services/cleaner"
import { useEffect } from "react"
@ -15,6 +16,9 @@ export const UserProvider = () => {
handle: session.user.handle,
})
tipcClient?.trackerIdentify({
user: session.user,
})
CleanerService.cleanRemainingData(session.user.id)
}, [session?.user, setUser])