-
Notifications
You must be signed in to change notification settings - Fork 586
/
Copy pathinstall-button-connected.tsx
161 lines (145 loc) · 5.33 KB
/
install-button-connected.tsx
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
import prettyBytes from 'pretty-bytes'
import {forwardRef, useImperativeHandle, useState} from 'react'
import {useTimeout} from 'react-use'
import semver from 'semver'
import {arrayIncludes} from 'ts-extras'
import {useAppInstall} from '@/hooks/use-app-install'
import {useLaunchApp} from '@/hooks/use-launch-app'
import {useVersion} from '@/hooks/use-version'
import {OSUpdateRequiredDialog} from '@/modules/app-store/os-update-required'
import {SelectDependenciesDialog} from '@/modules/app-store/select-dependencies-dialog'
import {useApps} from '@/providers/apps'
import {useAllAvailableApps} from '@/providers/available-apps'
import {installedStates, RegistryApp} from '@/trpc/trpc'
import {InstallButton} from './install-button'
export const InstallButtonConnected = forwardRef(
(
{
app,
}: {
app: RegistryApp
},
ref,
) => {
const appInstall = useAppInstall(app.id)
const {apps} = useAllAvailableApps()
const [showDepsDialog, setShowDepsDialog] = useState(false)
const [showOSUpdateRequiredDialog, setShowOSUpdateRequiredDialog] = useState(false)
const {userAppsKeyed, isLoading} = useApps()
const openApp = useLaunchApp()
const [selections, setSelections] = useState({} as Record<string, string>)
const os = useVersion()
const [show] = useTimeout(400)
const [highlightDependency, setHighlightDependency] = useState<string | undefined>(undefined)
useImperativeHandle(ref, () => ({
triggerInstall(highlightDependency?: string) {
setHighlightDependency(highlightDependency)
triggerInstall()
},
}))
if (!show() || isLoading || !userAppsKeyed || !apps || os.isLoading) {
return (
<InstallButton
key={app.id}
installSize={app.installSize ? prettyBytes(app.installSize) : undefined}
progress={appInstall.progress}
state='loading'
/>
)
}
const isInstalled = (appId: string) => arrayIncludes(installedStates, userAppsKeyed[appId]?.state)
const selectAlternative = (dependencyId: string, appId: string | undefined) => {
if (appId) selections[dependencyId] = appId
else delete selections[dependencyId]
setSelections({...selections})
}
const getAppsImplementing = (dependencyId: string) =>
apps
// Filter out community apps that aren't installed
.filter((registryApp) => {
const isCommunityApp = registryApp.appStoreId !== 'umbrel-app-store'
return !isCommunityApp || userAppsKeyed[registryApp.id]
})
// Prefer installed app over registry app
.map((registryApp) => userAppsKeyed[registryApp.id] ?? registryApp)
.filter((applicableApp) => applicableApp.implements?.includes(dependencyId))
.map((implementingApp) => implementingApp.id)
// Obtain possible alternatives for each dependency. Groups alternatives for
// each dependency into a two dimensional array, where each item references
// both the original dependency and the alterantive app. First item always is
// the original dependency.
// [
// [{dependencyId, appId: dependencyId}, {dependencyId, appId: implementingId}],
// [{dependencyId, appId: dependencyId}],
// ]
const dependencies = (app.dependencies ?? []).map((dependencyId) =>
[dependencyId, ...getAppsImplementing(dependencyId)].map((appId) => ({
dependencyId,
appId,
})),
)
// Auto-select the first installed alternative, naturally preferring the original
// app when it is installed as well.
dependencies.forEach((alternatives) => {
alternatives.forEach(({dependencyId, appId}) => {
if (!selections[dependencyId] && isInstalled(appId)) {
selectAlternative(dependencyId, appId)
}
})
})
// TODO: Also check if app is ready? `&& userAppsKeyed[dep].state === 'ready'`
// Will want to mark apps as in progress so we don't show that an app needs to be installed first
const areAllAlternativesSelectedAndInstalled = dependencies.every((alternatives) =>
alternatives.some((app) => selections[app.dependencyId] === app.appId && isInstalled(app.appId)),
)
const compatible = semver.lte(app.manifestVersion, os.version)
const install = () => {
if (!compatible) {
setShowOSUpdateRequiredDialog(true)
return
}
if (dependencies.length > 0) {
return setShowDepsDialog(true)
}
appInstall.install()
}
function triggerInstall() {
install()
}
const verifyInstall = (selectedDeps: Record<string, string>) => {
// Currently always the case because AppPermissionsDialog checks
if (areAllAlternativesSelectedAndInstalled) {
appInstall.install(selectedDeps)
}
}
return (
<>
<InstallButton
// `key` to prevent framer-motion from thinking install buttons from different pages are the same and animating between them
key={app.id}
installSize={app.installSize ? prettyBytes(app.installSize) : undefined}
// progress={userApp?.installProgress}
// state={userApp?.state || 'initial'}
progress={appInstall.progress}
state={appInstall.state}
compatible={compatible}
onInstallClick={install}
onOpenClick={() => openApp(app.id)}
/>
<SelectDependenciesDialog
appId={app.id}
dependencies={dependencies}
open={showDepsDialog}
onOpenChange={setShowDepsDialog}
onNext={verifyInstall}
highlightDependency={highlightDependency}
/>
<OSUpdateRequiredDialog
app={app}
open={showOSUpdateRequiredDialog}
onOpenChange={setShowOSUpdateRequiredDialog}
/>
</>
)
},
)