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:
parent
14b7eeaf5d
commit
cdc8703869
|
|
@ -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)
|
||||
}
|
||||
236
src/main/tipc.ts
236
src/main/tipc.ts
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { tipc } from "@egoist/tipc/main"
|
||||
|
||||
export const t = tipc.create()
|
||||
|
||||
export type T = typeof t
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}),
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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!"])
|
||||
}),
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}) }
|
||||
|
|
@ -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,
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export const AutoUpdater = () => {
|
|||
|
||||
const handleClick = useCallback(() => {
|
||||
setUpdaterStatus(false)
|
||||
window.posthog?.capture("update_restart")
|
||||
|
||||
tipcClient?.quitAndInstall()
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue