Skip to content

Commit ee41569

Browse files
authored
Merge pull request #257 from ynput/EPIC/OP-7108_Addon-Market
Addon Market: Install and update addons from Ynput Cloud
2 parents 9d4f81f + 472e765 commit ee41569

Some content is hidden

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

45 files changed

+1845
-168
lines changed

public/favicon-32x32.png

1.24 KB
Loading

public/slack-icon.png

85.7 KB
Loading

src/app.jsx

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ import { GlobalContextMenu } from './components/GlobalContextMenu'
3333
import LoadingPage from './pages/LoadingPage'
3434
import { ConfirmDialog } from 'primereact/confirmdialog'
3535
import OnBoardingPage from './pages/OnBoarding'
36-
import ServerRestartBanner from './components/ServerRestartBanner'
3736
import useTooltip from './hooks/Tooltip/useTooltip'
37+
import MarketPage from './pages/MarketPage'
38+
import { RestartProvider } from './context/restartContext'
3839

3940
const App = () => {
4041
const user = useSelector((state) => state.user)
@@ -124,62 +125,74 @@ const App = () => {
124125
() => (
125126
<ErrorBoundary FallbackComponent={ErrorFallback}>
126127
<Suspense fallback={<LoadingPage />}>
127-
<ContextMenuProvider>
128-
<GlobalContextMenu />
129-
<BrowserRouter>
130-
<ServerRestartBanner />
131-
<ShortcutsProvider>
132-
<QueryParamProvider
133-
adapter={ReactRouter6Adapter}
134-
options={{
135-
updateType: 'replaceIn',
136-
}}
137-
>
138-
<Header />
139-
<ShareDialog />
140-
<ConfirmDialog />
141-
<Routes>
142-
<Route path="/" exact element={<Navigate replace to="/dashboard/tasks" />} />
143-
<Route
144-
path="/manageProjects"
145-
exact
146-
element={<Navigate replace to="/manageProjects/anatomy" />}
147-
/>
148-
149-
<Route path="/dashboard" element={<Navigate replace to="/dashboard/tasks" />} />
150-
<Route path="/dashboard/:module" exact element={<UserDashboardPage />} />
151-
152-
<Route path="/manageProjects/:module" element={<ProjectManagerPage />} />
153-
<Route path={'/projects/:projectName/:module'} element={<ProjectPage />} />
154-
<Route
155-
path={'/projects/:projectName/addon/:addonName'}
156-
element={<ProjectPage />}
157-
/>
158-
<Route
159-
path="/settings"
160-
exact
161-
element={<Navigate replace to="/settings/anatomyPresets" />}
162-
/>
163-
<Route path="/settings/:module" exact element={<SettingsPage />} />
164-
<Route path="/settings/addon/:addonName" exact element={<SettingsPage />} />
165-
<Route
166-
path="/services"
167-
element={
168-
<ProtectedRoute isAllowed={!isUser} redirectPath="/">
169-
<ServicesPage />
170-
</ProtectedRoute>
171-
}
172-
/>
173-
<Route path="/explorer" element={<ExplorerPage />} />
174-
<Route path="/doc/api" element={<APIDocsPage />} />
175-
<Route path="/profile" element={<ProfilePage />} />
176-
<Route path="/events" element={<EventsPage />} />
177-
<Route element={<ErrorPage code="404" />} />
178-
</Routes>
179-
</QueryParamProvider>
180-
</ShortcutsProvider>
181-
</BrowserRouter>
182-
</ContextMenuProvider>
128+
<RestartProvider>
129+
<ContextMenuProvider>
130+
<GlobalContextMenu />
131+
<BrowserRouter>
132+
<ShortcutsProvider>
133+
<QueryParamProvider
134+
adapter={ReactRouter6Adapter}
135+
options={{
136+
updateType: 'replaceIn',
137+
}}
138+
>
139+
<Header />
140+
<ShareDialog />
141+
<ConfirmDialog />
142+
<Routes>
143+
<Route path="/" exact element={<Navigate replace to="/dashboard/tasks" />} />
144+
<Route
145+
path="/manageProjects"
146+
exact
147+
element={<Navigate replace to="/manageProjects/anatomy" />}
148+
/>
149+
150+
<Route
151+
path="/dashboard"
152+
element={<Navigate replace to="/dashboard/tasks" />}
153+
/>
154+
<Route path="/dashboard/:module" exact element={<UserDashboardPage />} />
155+
156+
<Route path="/manageProjects/:module" element={<ProjectManagerPage />} />
157+
<Route path={'/projects/:projectName/:module'} element={<ProjectPage />} />
158+
<Route
159+
path={'/projects/:projectName/addon/:addonName'}
160+
element={<ProjectPage />}
161+
/>
162+
<Route
163+
path="/settings"
164+
exact
165+
element={<Navigate replace to="/settings/anatomyPresets" />}
166+
/>
167+
<Route path="/settings/:module" exact element={<SettingsPage />} />
168+
<Route path="/settings/addon/:addonName" exact element={<SettingsPage />} />
169+
<Route
170+
path="/services"
171+
element={
172+
<ProtectedRoute isAllowed={!isUser} redirectPath="/">
173+
<ServicesPage />
174+
</ProtectedRoute>
175+
}
176+
/>
177+
<Route
178+
path="/market"
179+
element={
180+
<ProtectedRoute isAllowed={!isUser} redirectPath="/">
181+
<MarketPage />
182+
</ProtectedRoute>
183+
}
184+
/>
185+
<Route path="/explorer" element={<ExplorerPage />} />
186+
<Route path="/doc/api" element={<APIDocsPage />} />
187+
<Route path="/profile" element={<ProfilePage />} />
188+
<Route path="/events" element={<EventsPage />} />
189+
<Route element={<ErrorPage code="404" />} />
190+
</Routes>
191+
</QueryParamProvider>
192+
</ShortcutsProvider>
193+
</BrowserRouter>
194+
</ContextMenuProvider>
195+
</RestartProvider>
183196
</Suspense>
184197
</ErrorBoundary>
185198
),
@@ -246,7 +259,7 @@ const App = () => {
246259

247260
// stuck on onboarding page
248261
if (window.location.pathname.startsWith('/onboarding')) {
249-
window.history.replaceState({}, document.title, '/settings/bundles?selected=latest')
262+
window.history.replaceState({}, document.title, '/settings/bundles?bundle=latest')
250263
return loadingComponent
251264
}
252265

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React, { useRef, useState } from 'react'
2+
import * as Styled from './AddonIcon.styled'
3+
import { classNames } from 'primereact/utils'
4+
import { Icon } from '@ynput/ayon-react-components'
5+
6+
const AddonIcon = ({ isPlaceholder, size, ...props }) => {
7+
const [imageLoading, setImageLoading] = useState(props.src)
8+
const [imageError, setImageError] = useState(false)
9+
const imgRef = useRef(null)
10+
11+
const handleLoad = () => {
12+
if (!imgRef.current) setImageError(true)
13+
setImageLoading(false)
14+
}
15+
16+
const isLoading = isPlaceholder || imageLoading
17+
return (
18+
<Styled.Icon className={classNames({ isLoading, isError: imageError })} $size={size}>
19+
{imageError || !props.src ? (
20+
<Icon icon="extension" />
21+
) : (
22+
<img {...props} onLoad={handleLoad} ref={imgRef} />
23+
)}
24+
</Styled.Icon>
25+
)
26+
}
27+
28+
export default AddonIcon
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getShimmerStyles } from '@ynput/ayon-react-components'
2+
import styled from 'styled-components'
3+
4+
export const Icon = styled.div`
5+
position: relative;
6+
&,
7+
img {
8+
width: ${({ $size }) => `${$size}px` || '64px'};
9+
height: ${({ $size }) => `${$size}px` || '64px'};
10+
}
11+
12+
img {
13+
object-fit: contain;
14+
}
15+
16+
&.isLoading {
17+
${getShimmerStyles(undefined, undefined, { opacity: 1 })}
18+
border-radius: 100%;
19+
img {
20+
opacity: 0;
21+
}
22+
overflow: hidden;
23+
}
24+
25+
.icon {
26+
font-size: ${({ $size }) => `${$size}px` || '64px'};
27+
}
28+
`
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { classNames } from 'primereact/utils'
2+
import * as Styled from './MarketAddonCard.styled'
3+
import Type from '/src/theme/typography.module.css'
4+
import AddonIcon from '../AddonIcon/AddonIcon'
5+
import { Icon } from '@ynput/ayon-react-components'
6+
import { upperFirst } from 'lodash'
7+
8+
const MarketAddonCard = ({
9+
title,
10+
name,
11+
latestVersion,
12+
author,
13+
icon,
14+
isSelected,
15+
isOfficial,
16+
isVerified,
17+
isInstalled,
18+
isOutdated,
19+
isPlaceholder,
20+
isInstalling,
21+
isFinished,
22+
onInstall,
23+
...props
24+
}) => {
25+
let state = 'install'
26+
if (isInstalled && !isOutdated) state = 'installed'
27+
if (isInstalled && isOutdated) state = 'update'
28+
if (isInstalling) state = 'installing'
29+
if (isFinished) state = 'finished'
30+
31+
let stateIcon = null
32+
if (isInstalling) stateIcon = 'sync'
33+
if (isFinished) stateIcon = 'check_circle'
34+
35+
let stateVariant = 'light'
36+
if (state === 'install') stateVariant = 'surface'
37+
if (state === 'update') stateVariant = 'filled'
38+
39+
const handleActionClick = () => {
40+
if (['install', 'update'].includes(state)) {
41+
onInstall(name, latestVersion)
42+
}
43+
}
44+
45+
return (
46+
<Styled.Container {...props} className={classNames({ isSelected, isPlaceholder })}>
47+
<AddonIcon isPlaceholder={isPlaceholder} size={32} src={icon} alt={title + ' icon'} />
48+
<Styled.Content className="content">
49+
<Styled.TitleWrapper className="header">
50+
<Styled.Title className={Type.titleMedium}>{title}</Styled.Title>
51+
{isOfficial && <img src="/favicon-32x32.png" width={15} height={15} />}
52+
{isVerified && !isOfficial && (
53+
<Icon icon="new_release" style={{ color: ' var(--md-sys-color-secondary)' }} />
54+
)}
55+
</Styled.TitleWrapper>
56+
<Styled.AuthorWrapper className="details">
57+
<Styled.Author className={Type.labelMedium}>{author}</Styled.Author>
58+
</Styled.AuthorWrapper>
59+
</Styled.Content>
60+
{!isPlaceholder && (
61+
<Styled.Buttons>
62+
<Styled.Tag variant={stateVariant} className={state} onClick={handleActionClick}>
63+
{stateIcon && <Icon icon={stateIcon} />}
64+
{upperFirst(state)}
65+
</Styled.Tag>
66+
</Styled.Buttons>
67+
)}
68+
</Styled.Container>
69+
)
70+
}
71+
72+
export default MarketAddonCard
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Button, getShimmerStyles } from '@ynput/ayon-react-components'
2+
import styled, { keyframes } from 'styled-components'
3+
4+
export const Container = styled.div`
5+
padding: 6px 12px;
6+
background-color: var(--md-sys-color-surface-container-low);
7+
border: 1px solid var(--md-sys-color-surface-container-low);
8+
border-bottom-color: var(--md-sys-color-outline-variant);
9+
display: inline-flex;
10+
justify-content: start;
11+
align-items: center;
12+
gap: 12px;
13+
position: relative;
14+
z-index: 1;
15+
16+
cursor: pointer;
17+
18+
&:hover {
19+
background-color: var(--md-sys-color-surface-container-low-hover);
20+
}
21+
22+
&.isSelected {
23+
background-color: var(--md-sys-color-primary-container);
24+
25+
border-radius: var(--border-radius-m);
26+
border-color: var(--md-sys-color-primary);
27+
}
28+
29+
&.isPlaceholder {
30+
${getShimmerStyles()}
31+
32+
.content {
33+
border-radius: var(--border-radius-m);
34+
overflow: hidden;
35+
position: relative;
36+
${getShimmerStyles(undefined, undefined, { opacity: 1 })}
37+
}
38+
}
39+
`
40+
41+
export const Content = styled.div`
42+
display: inline-flex;
43+
flex-direction: column;
44+
justify-content: start;
45+
align-items: start;
46+
`
47+
48+
export const TitleWrapper = styled.div`
49+
display: inline-flex;
50+
justify-content: center;
51+
align-items: center;
52+
gap: var(--base-gap-small);
53+
`
54+
55+
export const Title = styled.div``
56+
57+
export const AuthorWrapper = styled.div`
58+
display: inline-flex;
59+
justify-content: center;
60+
align-items: center;
61+
`
62+
63+
export const Author = styled.div``
64+
65+
export const Buttons = styled.div`
66+
display: inline-flex;
67+
justify-content: flex-end;
68+
flex: 1;
69+
`
70+
71+
const SpinAnimation = keyframes`
72+
from {
73+
transform: rotate(0deg);
74+
}
75+
to {
76+
transform: rotate(360deg)
77+
}
78+
`
79+
80+
export const Tag = styled(Button)`
81+
border-radius: var(--border-radius-l);
82+
min-width: 75px;
83+
84+
gap: var(--base-gap-small);
85+
86+
&.installed,
87+
&.installing,
88+
&.finished {
89+
background-color: unset;
90+
user-select: none;
91+
}
92+
93+
&.installed {
94+
opacity: 0.5;
95+
font-style: italic;
96+
}
97+
98+
&.installing .icon {
99+
user-select: none;
100+
animation: ${SpinAnimation} 1s linear infinite;
101+
}
102+
103+
&.finished {
104+
user-select: none;
105+
}
106+
`

0 commit comments

Comments
 (0)