Skip to content

Commit fb5514d

Browse files
committed
Merge branch 'dev' into feature/ai
2 parents bafb6f8 + 1fdf482 commit fb5514d

42 files changed

Lines changed: 751 additions & 211 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-desktop.yml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ env:
3232
VITE_OPENPANEL_CLIENT_ID: ${{ vars.VITE_OPENPANEL_CLIENT_ID }}
3333
VITE_OPENPANEL_API_URL: ${{ vars.VITE_OPENPANEL_API_URL }}
3434
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
35+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
3536
NODE_OPTIONS: --max-old-space-size=8192
3637

3738
jobs:
@@ -137,14 +138,11 @@ jobs:
137138
- name: Build - Vite
138139
working-directory: apps/desktop
139140
run: pnpm build:electron-vite ${{ env.PROD == 'false' && '--mode staging' || '' }}
140-
env:
141-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
141+
142142
- name: Build - Windows and Linux
143-
if: runner.os != 'macOS'
143+
if: runner.os == 'Linux' || (runner.os == 'Windows' && github.event.inputs.store != 'true')
144144
working-directory: apps/desktop
145145
run: pnpm build:electron-forge ${{ env.PROD == 'false' && '--mode=staging' || '' }}
146-
env:
147-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
148146

149147
- name: Build - macOS
150148
if: runner.os == 'macOS' && github.event.inputs.store != 'true'
@@ -174,12 +172,15 @@ jobs:
174172
working-directory: apps/desktop
175173
run: pnpm build:electron-forge:mas
176174

175+
- name: Build - Microsoft Store
176+
if: runner.os == 'Windows' && github.event.inputs.store == 'true'
177+
working-directory: apps/desktop
178+
run: pnpm build:electron-forge:ms
179+
177180
- name: Build - Renderer
178181
if: runner.os == 'Linux'
179182
working-directory: apps/desktop
180183
run: pnpm build:render
181-
env:
182-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
183184

184185
- name: Upload file (macos-arm64-dmg)
185186
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ apps/desktop/src/renderer/dev-dist
2323
tsconfig.tsbuildinfo
2424
buildServer.json
2525

26-
**/**/generated-routes.ts
26+
**/**/generated-routes.ts
27+
28+
apps/desktop/build/appxmanifest.xml
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Package
3+
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
4+
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
5+
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
6+
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest">
7+
<Identity Name="{identityName}"
8+
ProcessorArchitecture="x64"
9+
Publisher="{publisherName}"
10+
Version="{packageVersion}" />
11+
<Properties>
12+
<DisplayName>{packageDisplayName}</DisplayName>
13+
<PublisherDisplayName>{publisherDisplayName}</PublisherDisplayName>
14+
<Description>No description entered</Description>
15+
<Logo>assets\SampleAppx.50x50.png</Logo>
16+
</Properties>
17+
<Resources>
18+
<Resource Language="en-us" />
19+
<Resource Language="zh-cn" />
20+
<Resource Language="zh-tw" />
21+
<Resource Language="ja" />
22+
</Resources>
23+
<Dependencies>
24+
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14316.0" MaxVersionTested="10.0.14316.0" />
25+
</Dependencies>
26+
<Capabilities>
27+
<rescap:Capability Name="runFullTrust"/>
28+
</Capabilities>
29+
<Applications>
30+
<Application Id="{packageName}" Executable="{packageExecutable}" EntryPoint="Windows.FullTrustApplication">
31+
<uap:VisualElements
32+
BackgroundColor="{packageBackgroundColor}"
33+
DisplayName="{packageDisplayName}"
34+
Square150x150Logo="assets\SampleAppx.150x150.png"
35+
Square44x44Logo="assets\SampleAppx.44x44.png"
36+
Description="{packageDescription}">
37+
<uap:DefaultTile Wide310x150Logo="assets\SampleAppx.310x150.png" />
38+
</uap:VisualElements>
39+
<Extensions>{protocol}
40+
</Extensions>
41+
</Application>
42+
</Applications>
43+
</Package>

apps/desktop/forge.config.cts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { rimraf, rimrafSync } from "rimraf"
1818

1919
const platform = process.argv.find((arg) => arg.startsWith("--platform"))?.split("=")[1]
2020
const mode = process.argv.find((arg) => arg.startsWith("--mode"))?.split("=")[1]
21+
const isMicrosoftStore =
22+
process.argv.find((arg) => arg.startsWith("--ms"))?.split("=")[1] === "true"
2123

2224
const isStaging = mode === "staging"
2325

@@ -185,11 +187,6 @@ const config: ForgeConfig = {
185187
},
186188
["darwin", "mas"],
187189
),
188-
new MakerSquirrel({
189-
name: "Folo",
190-
setupIcon: isStaging ? "resources/icon-staging.ico" : "resources/icon.ico",
191-
iconUrl: "https://app.folo.is/favicon.ico",
192-
}),
193190
new MakerAppImage({
194191
config: {
195192
icons: [
@@ -207,17 +204,29 @@ const config: ForgeConfig = {
207204
},
208205
["mas"],
209206
),
210-
new MakerAppX({
211-
publisher: "CN=7CBBEB6A-9B0E-4387-BAE3-576D0ACA279E",
212-
packageDisplayName: "Folo - Follow everything in one place",
213-
devCert: "build/dev.pfx",
214-
assets: "static/appx",
215-
// @ts-ignore
216-
publisherDisplayName: "Natural Selection Labs",
217-
identityName: "NaturalSelectionLabs.Follow-Yourfavoritesinoneinbo",
218-
packageBackgroundColor: "#FF5C00",
219-
protocol: "folo",
220-
}),
207+
// Only include AppX maker for Microsoft Store builds
208+
...(isMicrosoftStore
209+
? [
210+
new MakerAppX({
211+
publisher: "CN=7CBBEB6A-9B0E-4387-BAE3-576D0ACA279E",
212+
packageDisplayName: "Folo - Follow everything in one place",
213+
devCert: "build/dev.pfx",
214+
assets: "static/appx",
215+
manifest: "build/appxmanifest.xml",
216+
// @ts-ignore
217+
publisherDisplayName: "Natural Selection Labs",
218+
identityName: "NaturalSelectionLabs.Follow-Yourfavoritesinoneinbo",
219+
packageBackgroundColor: "#FF5C00",
220+
protocol: "folo",
221+
}),
222+
]
223+
: [
224+
new MakerSquirrel({
225+
name: "Folo",
226+
setupIcon: isStaging ? "resources/icon-staging.ico" : "resources/icon.ico",
227+
iconUrl: "https://app.folo.is/favicon.ico",
228+
}),
229+
]),
221230
],
222231
plugins: [
223232
// Fuses are used to enable/disable various Electron functionality

apps/desktop/layer/main/src/ipc/services/integration.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import fsp from "node:fs/promises"
33

44
import path from "pathe"
55

6+
import { store } from "~/lib/store"
7+
68
import type { IpcContext } from "../base"
79
import { IpcMethod, IpcService } from "../base"
810

@@ -26,6 +28,22 @@ interface SaveToEagleInput {
2628
url: string
2729
mediaUrls: string[]
2830
}
31+
32+
interface LoginToQBittorrentInput {
33+
host: string
34+
username: string
35+
password: string
36+
}
37+
38+
interface CheckQBittorrentAuthInput {
39+
host: string
40+
}
41+
42+
interface AddMagnetInput {
43+
host: string
44+
urls: string[]
45+
}
46+
2947
export class IntegrationService extends IpcService {
3048
constructor() {
3149
super("integration")
@@ -98,4 +116,83 @@ ${content}
98116
return null
99117
}
100118
}
119+
120+
@IpcMethod()
121+
async loginToQBittorrent(context: IpcContext, input: LoginToQBittorrentInput) {
122+
const { host, username, password } = input
123+
124+
const existingSID = store.get("qbittorrentSID")
125+
if (existingSID) {
126+
const errorMessage = await this.checkQBittorrentAuth(context, { host })
127+
if (!errorMessage) {
128+
return
129+
}
130+
}
131+
132+
const res = await fetch(`${host}/api/v2/auth/login`, {
133+
method: "POST",
134+
headers: {
135+
"Content-Type": "application/x-www-form-urlencoded",
136+
},
137+
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`,
138+
})
139+
140+
if (!res.ok) {
141+
return `Failed to log in to qBittorrent: ${await res.text()}`
142+
}
143+
144+
const cookies = res.headers.get("set-cookie") || ""
145+
const match = cookies.match(/SID=([^;]+)/)
146+
if (!match || !match[1]) {
147+
return "Failed to get SID from qBittorrent"
148+
}
149+
150+
store.set("qbittorrentSID", match[1])
151+
return
152+
}
153+
154+
async checkQBittorrentAuth(context: IpcContext, input: CheckQBittorrentAuthInput) {
155+
const { host } = input
156+
const sid = store.get("qbittorrentSID")
157+
if (!sid) {
158+
return "Not logged in to qBittorrent"
159+
}
160+
const res = await fetch(`${host}/api/v2/auth/check`, {
161+
method: "GET",
162+
headers: {
163+
Cookie: `SID=${sid}`,
164+
},
165+
credentials: "omit",
166+
})
167+
168+
if (!res.ok) {
169+
return await res.text()
170+
}
171+
}
172+
173+
@IpcMethod()
174+
async addMagnet(context: IpcContext, input: AddMagnetInput) {
175+
const { host, urls } = input
176+
const sid = store.get("qbittorrentSID")
177+
if (!sid) {
178+
return "Not logged in to qBittorrent"
179+
}
180+
const res = await fetch(`${host}/api/v2/torrents/add`, {
181+
method: "POST",
182+
headers: {
183+
"Content-Type": "application/x-www-form-urlencoded",
184+
Cookie: `SID=${sid}`,
185+
},
186+
credentials: "omit",
187+
body: `urls=${encodeURIComponent(urls.join("\n"))}`,
188+
})
189+
190+
if (!res.ok) {
191+
const text = await res.text()
192+
return `Failed to add magnet links: ${text}`
193+
}
194+
195+
// eslint-disable-next-line no-console
196+
console.log(`Added magnet links to qBittorrent: ${urls.join(", ")}`)
197+
}
101198
}

apps/desktop/layer/main/src/lib/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type StoreData = {
99
cacheSizeLimit?: number | null
1010
minimizeToTray?: boolean | null
1111
proxy?: string | null
12+
qbittorrentSID?: string | null
1213
user?: string | null
1314
windowState?: {
1415
height: number

apps/desktop/layer/main/src/updater/configs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DEV, MODE, ModeEnum } from "@follow/shared/constants"
1+
import { DEV, MICROSOFT_STORE_BUILD, MODE, ModeEnum } from "@follow/shared/constants"
22
import path from "pathe"
33

44
import { isWindows } from "../env"
@@ -8,7 +8,7 @@ export const appUpdaterConfig = {
88
enableRenderHotUpdate: !DEV && MODE !== ModeEnum.staging,
99
enableCoreUpdate:
1010
!process.mas &&
11-
!process.windowsStore &&
11+
!MICROSOFT_STORE_BUILD &&
1212
// Disable core update if platfrom is windows and application is't executing from default installion path.
1313
// If the process is not executed from the default installation path,
1414
// it is usually managed through a package manager like Scoop.

apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding
3131
import type { FollowCommandId } from "~/modules/command/types"
3232
import { useToolbarOrderMap } from "~/modules/customize-toolbar/hooks"
3333

34+
import { useRouteParams } from "./useRouteParams"
35+
3436
export const enableEntryReadability = async ({ id, url }: { id: string; url: string }) => {
3537
const status = getReadabilityStatus()[id]
3638
const isTurnOn = status !== ReadabilityStatus.INITIAL && !!status
@@ -126,6 +128,7 @@ const entrySelector = (state: EntryModel) => {
126128
const { summary, translation, readability } = state.settings || {}
127129

128130
const media = state.media || []
131+
const attachments = state.attachments || []
129132
const images = media.filter((a) => a.type === "photo")
130133
const imagesLength = images.length
131134

@@ -141,6 +144,7 @@ const entrySelector = (state: EntryModel) => {
141144
hasContent,
142145
doesContentContainsHTMLTags,
143146
imagesLength,
147+
hasBitTorrent: attachments.some((a) => a.mime_type === "application/x-bittorrent"),
144148
}
145149
}
146150
export const HIDE_ACTIONS_IN_ENTRY_CONTEXT_MENU = [
@@ -165,6 +169,7 @@ export const useEntryActions = ({
165169
compact?: boolean
166170
}) => {
167171
const entry = useEntry(entryId, entrySelector)
172+
const { isCollection } = useRouteParams()
168173
const isInCollection = useIsEntryStarred(entryId)
169174
const isEntryInReadability = useEntryIsInReadability(entryId)
170175

@@ -236,6 +241,12 @@ export const useEntryActions = ({
236241
onClick: runCmdFn(COMMAND_ID.integration.saveToZotero, [{ entryId }]),
237242
entryId,
238243
}),
244+
new EntryActionMenuItem({
245+
id: COMMAND_ID.integration.saveToQBittorrent,
246+
onClick: runCmdFn(COMMAND_ID.integration.saveToQBittorrent, [{ entryId }]),
247+
hide: !IN_ELECTRON || !entry.hasBitTorrent,
248+
entryId,
249+
}),
239250
new EntryActionMenuItem({
240251
id: COMMAND_ID.entry.tip,
241252
onClick: runCmdFn(COMMAND_ID.entry.tip, [
@@ -326,21 +337,21 @@ export const useEntryActions = ({
326337
new EntryActionMenuItem({
327338
id: COMMAND_ID.entry.readAbove,
328339
onClick: runCmdFn(COMMAND_ID.entry.readAbove, [{ publishedAt: entry.publishedAt }]),
329-
hide: !!isInCollection,
340+
hide: !!isCollection,
330341
entryId,
331342
}),
332343
new EntryActionMenuItem({
333344
id: COMMAND_ID.entry.read,
334345
onClick: runCmdFn(COMMAND_ID.entry.read, [{ entryId }]),
335-
hide: !!isInCollection,
346+
hide: !!isCollection,
336347
active: !!entry.read,
337348
shortcut: shortcuts[COMMAND_ID.entry.read],
338349
entryId,
339350
}),
340351
new EntryActionMenuItem({
341352
id: COMMAND_ID.entry.readBelow,
342353
onClick: runCmdFn(COMMAND_ID.entry.readBelow, [{ publishedAt: entry.publishedAt }]),
343-
hide: !!isInCollection,
354+
hide: !!isCollection,
344355
entryId,
345356
}),
346357
MENU_ITEM_SEPARATOR,
@@ -392,20 +403,22 @@ export const useEntryActions = ({
392403
hasEntry,
393404
runCmdFn,
394405
entryId,
395-
feed?.id,
396-
feed?.ownerUserId,
397-
feed?.siteUrl,
398-
isInbox,
399-
shortcuts,
400-
view,
401-
isInCollection,
406+
entry?.hasBitTorrent,
402407
entry?.url,
403408
entry?.imagesLength,
404409
entry?.publishedAt,
405410
entry?.read,
406411
entry?.hasContent,
407412
entry?.readability,
408413
entry?.doesContentContainsHTMLTags,
414+
feed?.id,
415+
feed?.ownerUserId,
416+
feed?.siteUrl,
417+
isInbox,
418+
shortcuts,
419+
view,
420+
isCollection,
421+
isInCollection,
409422
isShowSourceContent,
410423
isShowAISummaryAuto,
411424
isShowAISummaryOnce,

0 commit comments

Comments
 (0)