Skip to content

Commit 4703e3f

Browse files
authored
Added Stash Notifications (#537)
1 parent 319ec3d commit 4703e3f

File tree

4 files changed

+321
-0
lines changed

4 files changed

+321
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Stash Notifications
2+
3+
Receive notifications about plugin and scraper changes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.navbar-buttons .notification-btn-container {
2+
position: relative;
3+
display: inline-block;
4+
margin-top: 3px;
5+
}
6+
.navbar-buttons .notification-btn-container .dropdown-menu-end {
7+
right: 0 !important;
8+
left: auto !important;
9+
width: 300px;
10+
max-width: 300px;
11+
white-space: normal;
12+
word-break: break-word;
13+
}
14+
.navbar-buttons .notification-btn-container .dropdown-menu-end a.dropdown-item {
15+
white-space: normal;
16+
}
17+
.navbar-buttons .notification-btn-container .notification-btn {
18+
max-width: 45px;
19+
}
20+
.navbar-buttons .notification-btn-container .notification-btn .notification-badge {
21+
position: relative;
22+
right: 10px;
23+
top: -10px !important;
24+
}
25+
26+
.stash-notification-modal h5 {
27+
margin-top: 0.5em;
28+
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
"use strict";
2+
(() => {
3+
// src/globals.ts
4+
var api = window.PluginApi;
5+
var { React, ReactDOM, GQL, utils, libraries, patch, components } = api;
6+
7+
// src/hooks/useNotifications.tsx
8+
var NotificationContext = React.createContext({
9+
notifications: [],
10+
setNotifications: (n) => {
11+
}
12+
});
13+
var NotificationProvider = ({
14+
children
15+
}) => {
16+
const [notifications, setNotifications] = React.useState(
17+
[]
18+
);
19+
return /* @__PURE__ */ React.createElement(
20+
NotificationContext.Provider,
21+
{
22+
value: { notifications, setNotifications }
23+
},
24+
children
25+
);
26+
};
27+
var useNotifications = () => React.useContext(NotificationContext);
28+
29+
// src/types/INotification.ts
30+
var NotificationType = /* @__PURE__ */ ((NotificationType2) => {
31+
NotificationType2[NotificationType2["Plugin"] = 0] = "Plugin";
32+
NotificationType2[NotificationType2["Scraper"] = 1] = "Scraper";
33+
return NotificationType2;
34+
})(NotificationType || {});
35+
36+
// src/hooks/usePackageUpdateChecking.ts
37+
var getUpdateNotifications = (packages, type) => {
38+
if (!packages)
39+
return [];
40+
return packages.filter(
41+
(pkg) => pkg.source_package && pkg.version !== pkg.source_package.version
42+
).map((pkg) => ({
43+
message: `${NotificationType[type]} ${pkg.name} can be updated to version ${pkg.source_package?.version}`,
44+
data: pkg,
45+
type
46+
}));
47+
};
48+
var usePackageUpdateChecking = () => {
49+
const plugins = GQL.useInstalledPluginPackagesStatusQuery();
50+
const scrapers = GQL.useInstalledScraperPackagesStatusQuery();
51+
const loading = plugins.loading || scrapers.loading;
52+
const pluginNotifications = getUpdateNotifications(
53+
plugins.data?.installedPackages,
54+
0 /* Plugin */
55+
);
56+
const scraperNotifications = getUpdateNotifications(
57+
scrapers.data?.installedPackages,
58+
1 /* Scraper */
59+
);
60+
return {
61+
loading,
62+
packages: [...pluginNotifications, ...scraperNotifications]
63+
};
64+
};
65+
66+
// src/components/modal/PackageModalBody.tsx
67+
var PackageModalBody = ({ pkg }) => {
68+
const { FormattedDate, FormattedTime } = libraries.Intl;
69+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h4", null, pkg.name), /* @__PURE__ */ React.createElement("div", null, "id: ", pkg.package_id), /* @__PURE__ */ React.createElement("div", null, "source url: ", pkg.sourceURL)), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h5", null, "Current Version (", pkg.version, ")"), /* @__PURE__ */ React.createElement("div", null, "Last updated:", " ", /* @__PURE__ */ React.createElement(
70+
FormattedDate,
71+
{
72+
value: pkg.date,
73+
format: "short",
74+
timeZone: "utc"
75+
}
76+
), " ", /* @__PURE__ */ React.createElement(
77+
FormattedTime,
78+
{
79+
value: pkg.date,
80+
hour: "numeric",
81+
minute: "numeric",
82+
second: "numeric",
83+
timeZone: "utc"
84+
}
85+
))), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h5", null, "Latest Version (", pkg.source_package?.version, ")"), /* @__PURE__ */ React.createElement("div", null, "Remote version updated:", " ", /* @__PURE__ */ React.createElement(
86+
FormattedDate,
87+
{
88+
value: pkg.source_package?.date,
89+
format: "short",
90+
timeZone: "utc"
91+
}
92+
), " ", /* @__PURE__ */ React.createElement(
93+
FormattedTime,
94+
{
95+
value: pkg.source_package?.date,
96+
hour: "numeric",
97+
minute: "numeric",
98+
second: "numeric",
99+
timeZone: "utc"
100+
}
101+
))));
102+
};
103+
var PackageModalBody_default = PackageModalBody;
104+
105+
// src/components/modal/PluginNotification.tsx
106+
var PluginNotification = ({ notification, onClose }) => {
107+
const pkg = notification.data;
108+
const { Button, Modal } = libraries.Bootstrap;
109+
const { StashService } = utils;
110+
const { notifications, setNotifications } = useNotifications();
111+
const removePluginNotifications = (packageIds) => {
112+
setNotifications(
113+
notifications.filter((n) => {
114+
const pkg2 = n.data;
115+
return !packageIds.includes(pkg2?.package_id);
116+
})
117+
);
118+
};
119+
const updatePlugin = async () => {
120+
const vars = [{ id: pkg.package_id, sourceURL: pkg.sourceURL }];
121+
await StashService.mutateUpdatePluginPackages(vars);
122+
removePluginNotifications([pkg.package_id]);
123+
onClose();
124+
};
125+
const updateAllPlugins = async () => {
126+
await StashService.mutateUpdatePluginPackages([]);
127+
setNotifications(
128+
notifications.filter((n) => n.type !== 0 /* Plugin */)
129+
);
130+
onClose();
131+
};
132+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Modal.Header, { closeButton: true }, /* @__PURE__ */ React.createElement(Modal.Title, null, "Update Plugin")), /* @__PURE__ */ React.createElement(Modal.Body, null, /* @__PURE__ */ React.createElement(PackageModalBody_default, { pkg })), /* @__PURE__ */ React.createElement(Modal.Footer, null, /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: updatePlugin }, "Update Plugin"), /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: updateAllPlugins }, "Update All Plugins"), /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: onClose }, "Cancel")));
133+
};
134+
var PluginNotification_default = PluginNotification;
135+
136+
// src/components/modal/NotificationModal.tsx
137+
var NotificationModal = ({ display, onClose, content }) => {
138+
const { Button, Modal } = libraries.Bootstrap;
139+
if (!display)
140+
return null;
141+
return /* @__PURE__ */ React.createElement(
142+
Modal,
143+
{
144+
className: "stash-notification-modal",
145+
show: display,
146+
onHide: onClose,
147+
centered: true
148+
},
149+
typeof content === "string" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Modal.Header, { closeButton: true }, /* @__PURE__ */ React.createElement(Modal.Title, null, "Notification")), /* @__PURE__ */ React.createElement(Modal.Body, null, content), /* @__PURE__ */ React.createElement(Modal.Footer, null, /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: onClose }, "Cancel"))) : content
150+
);
151+
};
152+
var NotificationModal_default = NotificationModal;
153+
154+
// src/components/modal/ScraperNotification.tsx
155+
var ScraperNotification = ({ notification, onClose }) => {
156+
const pkg = notification.data;
157+
const { Button, Modal } = libraries.Bootstrap;
158+
const { StashService } = utils;
159+
const { notifications, setNotifications } = useNotifications();
160+
const removeScraperNotifications = (packageIds) => {
161+
setNotifications(
162+
notifications.filter((n) => {
163+
const pkg2 = n.data;
164+
return !packageIds.includes(pkg2?.package_id);
165+
})
166+
);
167+
};
168+
const updateScraper = async () => {
169+
const vars = [{ id: pkg.package_id, sourceURL: pkg.sourceURL }];
170+
await StashService.mutateUpdateScraperPackages(vars);
171+
removeScraperNotifications([pkg.package_id]);
172+
onClose();
173+
};
174+
const updateAllScrapers = async () => {
175+
await StashService.mutateUpdateScraperPackages([]);
176+
setNotifications(
177+
notifications.filter((n) => n.type !== 1 /* Scraper */)
178+
);
179+
onClose();
180+
};
181+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Modal.Header, { closeButton: true }, /* @__PURE__ */ React.createElement(Modal.Title, null, "Update Scraper")), /* @__PURE__ */ React.createElement(Modal.Body, null, /* @__PURE__ */ React.createElement(PackageModalBody_default, { pkg })), /* @__PURE__ */ React.createElement(Modal.Footer, null, /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: updateScraper }, "Update Scraper"), /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: updateAllScrapers }, "Update All Scrapers"), /* @__PURE__ */ React.createElement(Button, { variant: "primary", onClick: onClose }, "Cancel")));
182+
};
183+
var ScraperNotification_default = ScraperNotification;
184+
185+
// src/components/NotificationDropdown.tsx
186+
var NotificationDropdown = ({ notifications }) => {
187+
const [display, setDisplay] = React.useState(false);
188+
const [activeNotification, setActiveNotification] = React.useState(null);
189+
const { Dropdown } = libraries.Bootstrap;
190+
const onNotificationClick = (notification) => {
191+
setActiveNotification(notification);
192+
setDisplay(true);
193+
};
194+
const onClose = () => {
195+
setDisplay(false);
196+
};
197+
const renderModal = () => {
198+
if (!activeNotification)
199+
return null;
200+
const modals = {
201+
[0 /* Plugin */]: /* @__PURE__ */ React.createElement(
202+
PluginNotification_default,
203+
{
204+
notification: activeNotification,
205+
onClose
206+
}
207+
),
208+
[1 /* Scraper */]: /* @__PURE__ */ React.createElement(
209+
ScraperNotification_default,
210+
{
211+
notification: activeNotification,
212+
onClose
213+
}
214+
)
215+
};
216+
return modals[activeNotification.type] ?? activeNotification.message;
217+
};
218+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Dropdown.Menu, { className: "dropdown-menu-end" }, notifications.map((n, i) => /* @__PURE__ */ React.createElement(
219+
Dropdown.Item,
220+
{
221+
key: i,
222+
onClick: () => onNotificationClick(n)
223+
},
224+
n.message
225+
))), activeNotification && /* @__PURE__ */ React.createElement(
226+
NotificationModal_default,
227+
{
228+
display,
229+
onClose,
230+
content: renderModal()
231+
}
232+
));
233+
};
234+
235+
// src/components/StashNotificationsButton.tsx
236+
var StashNotificationsButton = () => {
237+
const { Badge, Button, Dropdown } = libraries.Bootstrap;
238+
const { Icon } = components;
239+
const { faBell } = libraries.FontAwesomeSolid;
240+
const { notifications, setNotifications } = useNotifications();
241+
const { loading, packages } = usePackageUpdateChecking();
242+
const hasNotifications = !loading && notifications.length > 0;
243+
React.useEffect(() => {
244+
if (!loading && packages.length > 0) {
245+
setNotifications(packages);
246+
}
247+
}, [loading]);
248+
if (!hasNotifications)
249+
return null;
250+
return /* @__PURE__ */ React.createElement("span", { className: "notification-btn-container" }, /* @__PURE__ */ React.createElement(Dropdown, null, /* @__PURE__ */ React.createElement(
251+
Dropdown.Toggle,
252+
{
253+
as: Button,
254+
className: "nav-utility minimal notification-btn"
255+
},
256+
/* @__PURE__ */ React.createElement(Icon, { icon: faBell }),
257+
hasNotifications && /* @__PURE__ */ React.createElement(
258+
Badge,
259+
{
260+
className: "notification-badge",
261+
pill: true,
262+
variant: "danger"
263+
},
264+
notifications.length
265+
)
266+
), /* @__PURE__ */ React.createElement(NotificationDropdown, { notifications })));
267+
};
268+
var StashNotificationsButton_default = StashNotificationsButton;
269+
270+
// src/stashNotifications.tsx
271+
(function() {
272+
patch.before("MainNavBar.UtilityItems", function(props) {
273+
return [
274+
{
275+
children: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(NotificationProvider, null, /* @__PURE__ */ React.createElement(StashNotificationsButton_default, null)), props.children)
276+
}
277+
];
278+
});
279+
})();
280+
})();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: StashNotifications
2+
description: Receive notifications about plugin and scraper changes.
3+
version: 1.1
4+
ui:
5+
javascript:
6+
- stashNotifications.js
7+
css:
8+
- stashNotifications.css
9+
assets:
10+
/: .

0 commit comments

Comments
 (0)