feat: added notification system
mellbacon committed
May 21, 2024 at 13:18 UTC
e0d57aaae6667d6e6e5cf1a1825bedc9645a6db8
7 files changed
+282
-20
src/App.svelte
+3
-1
@@ -16,6 +16,7 @@
16
import { getExtensions } from "./config/extensionhandler";
17
import { fitTerminal } from "./lib/Terminal.svelte";
18
import { loadDir } from "./lib/File";
19
+ import NotificationToasts from "./lib/Notifications/NotificationToasts.svelte";
20
let resolution = writable(0);
21
22
let minPanelSize = 10;
@@ -130,7 +131,7 @@
131
</Pane>
132
{#if $editortool}
133
<Pane bind:size={bottomPanelSize} maxSize={90} minSize={10} class="view-bottom-pane">
133
- <ToolView content={$editortool.content} name={$editortool.name}></ToolView>
134
+ <ToolView content={$editortool.content} name={$editortool.name} options={$editortool.options} buttons={$editortool.buttons}></ToolView>
135
</Pane>
136
{/if}
137
</Splitpanes>
@@ -139,6 +140,7 @@
140
</Splitpanes>
141
</div>
142
<Statusbar />
143
+ <NotificationToasts />
144
</div>
145
146
{#if $_openPopup}
src/lib/Notifications/Notification.svelte
+33
-8
@@ -1,9 +1,10 @@
1
<script lang="ts">
2
import { getTime } from "../../config/config";
3
- import { updateReadStatus } from "./notifications";
3
+ import { closeNotification, updateReadStatus } from "./notifications";
4
5
export let id;
6
export let type = "";
7
+ export let title = "";
8
export let message = "";
9
export let read = false;
10
export let actions = [];
@@ -27,19 +28,25 @@
28
</span>
29
</div>
30
<div bind:this={notification} class="notification" class:error={type === "Error"} class:warning={type === "Warning"} class:read>
30
- <div class="details">
31
- <div class="icon"></div>
32
- <div class="message">{type}: {message}</div>
31
+ <div class="notification-info">
32
+ <div class="details">
33
+ <div class="icon"></div>
34
+ <div class="title">{type}: {title}</div>
35
+ </div>
36
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
37
+ <span class="close-button" on:click={() => {closeNotification(id)}}></span>
38
</div>
39
+
40
+ <div class="message">{message}</div>
41
+
42
+ {#if actions.length > 0}
43
<div class="actions">
44
{#each actions as action}
45
<!-- svelte-ignore a11y-click-events-have-key-events -->
46
<div class="action-button" on:click={action.action}>{action.label}</div>
47
{/each}
48
</div>
40
-
41
- <!-- svelte-ignore a11y-click-events-have-key-events -->
42
- <span class="close-button" on:click={() => {}}></span>
49
+ {/if}
50
</div>
51
52
<style lang="scss">
@@ -74,7 +81,8 @@
81
margin-bottom: 0.8rem;
82
position: relative;
83
border-top: 3px solid $default-color;
77
- justify-content: space-between;
84
+ flex-direction: column;
85
+ justify-content: center;
86
&.warning {
87
border-top: 3px solid $warning-color;
88
.icon {
@@ -99,6 +107,9 @@
107
:global(.bx--toast-notification__close-button .bx--toast-notification__close-icon) {
108
fill: #bdd4ff75 !important;
109
}
110
+ .message {
111
+ color: #333;
112
+ }
113
}
114
&::before {
115
position: absolute;
@@ -110,6 +121,11 @@
121
border-color: #333;
122
content: "";
123
}
124
+ .notification-info {
125
+ display: flex;
126
+ justify-content: space-between;
127
+ min-height: 3rem;
128
+ }
129
.details {
130
display: flex;
131
margin: 0 0.5rem;
@@ -124,12 +140,21 @@
140
margin-right: 10px;
141
}
142
}
143
+ .message {
144
+ font-size: 15px;
145
+ color: #6d6d6d;
146
+ margin: 0.5rem 0;
147
+ margin-left: 2.4rem;
148
+ margin-top: -0.5rem;
149
+ }
150
.actions {
151
display: flex;
152
margin: 0 0.5rem;
153
position: relative;
154
top: -1px;
155
align-items: center;
156
+ margin-left: 2.4rem;
157
+ margin-bottom: 0.5rem;
158
.action-button {
159
display: flex;
160
justify-content: center;
src/lib/Notifications/NotificationList.svelte
+8
-1
@@ -4,9 +4,13 @@
4
</script>
5
6
<div id="notifications">
7
+{#if $notifications.length == 0}
8
+ <span class="empty">You have no notifications</span>
9
+{:else}
10
{#each $notifications as notification}
8
- <Notification id={notification.id} type={notification.type} message={notification.message} read={notification.read} actions={notification.actions}></Notification>
11
+ <Notification id={notification.id} type={notification.type} title={notification.title} message={notification.message} read={notification.read} actions={notification.actions}></Notification>
12
{/each}
13
+{/if}
14
</div>
15
16
<style lang="scss">
@@ -15,4 +19,7 @@
19
padding: 5px;
20
overflow: auto;
21
}
22
+.empty {
23
+ color: #8c8c8c;
24
+}
25
</style>
\ No newline at end of file
src/lib/Notifications/NotificationToast.svelte
new
+149
@@ -0,0 +1,149 @@
1
+<script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { closeToast } from "./notifications";
4
+ import { fade } from "svelte/transition";
5
+
6
+ export let id: number;
7
+ export let type = "";
8
+ export let title = "";
9
+ export let message = "";
10
+ export let actions = [];
11
+
12
+ let notification: HTMLElement | null;
13
+
14
+ onMount(() => {
15
+ let timer = setTimeout(() => {
16
+ notification.remove();
17
+ clearTimeout(timer);
18
+ }, 1500000)
19
+ return () => {
20
+ clearTimeout(timer)
21
+ }
22
+ })
23
+</script>
24
+
25
+<div bind:this={notification} class="notification-toast" class:error={type === "Error"} class:warning={type === "Warning"} transition:fade={{ duration: 150 }}>
26
+ <div class="notification-info">
27
+ <div class="details">
28
+ <div class="icon"></div>
29
+ <div class="title">{type}: {title}</div>
30
+ </div>
31
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
32
+ <span class="close-button" on:click={() => {closeToast(id)}}></span>
33
+ </div>
34
+
35
+ <div class="message">{message}</div>
36
+
37
+ {#if actions.length > 0}
38
+ <div class="actions">
39
+ {#each actions as action}
40
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
41
+ <div class="action-button" on:click={action.action}>{action.label}</div>
42
+ {/each}
43
+ </div>
44
+ {/if}
45
+</div>
46
+
47
+<style lang="scss">
48
+ $notification-height: 3rem;
49
+ $notification-border-width: 1px;
50
+ $error-color: #c02b2b;
51
+ $warning-color: #ffeb10;
52
+ $default-color: #4589ff;
53
+ .notification-toast {
54
+ display: flex;
55
+ min-height: $notification-height;
56
+ background-color: #1f1f1f;
57
+ margin-bottom: 0.8rem;
58
+ position: relative;
59
+ border-top: 3px solid $default-color;
60
+ flex-direction: column;
61
+ justify-content: center;
62
+ box-shadow: 0 2px 6px #000000cc;
63
+ z-index: 10;
64
+ &.warning {
65
+ border-top: 3px solid $warning-color;
66
+ .icon {
67
+ background-color: $warning-color !important;
68
+ }
69
+ }
70
+ &.error {
71
+ border-top: 3px solid $error-color;
72
+ .icon {
73
+ background-color: $error-color !important;
74
+ }
75
+ }
76
+ &::before {
77
+ position: absolute;
78
+ height: 100%;
79
+ width: 100%;
80
+ border-width: 0 $notification-border-width $notification-border-width $notification-border-width;
81
+ border-style: solid;
82
+ box-sizing: border-box;
83
+ border-color: #333;
84
+ content: "";
85
+ }
86
+ .notification-info {
87
+ display: flex;
88
+ justify-content: space-between;
89
+ min-height: 3rem;
90
+ }
91
+ .details {
92
+ display: flex;
93
+ margin: 0 0.5rem;
94
+ position: relative;
95
+ top: -1px;
96
+ align-items: center;
97
+ .icon {
98
+ border-radius: 50%;
99
+ background-color: $default-color;
100
+ min-width: 20px;
101
+ min-height: 20px;
102
+ margin-right: 10px;
103
+ }
104
+ }
105
+ .message {
106
+ font-size: 15px;
107
+ color: #6d6d6d;
108
+ margin: 0.5rem 0;
109
+ margin-left: 2.4rem;
110
+ margin-top: -0.5rem;
111
+ }
112
+ .actions {
113
+ display: flex;
114
+ margin: 0 0.5rem;
115
+ position: relative;
116
+ top: -1px;
117
+ align-items: center;
118
+ margin-left: 2.4rem;
119
+ margin-bottom: 0.5rem;
120
+ .action-button {
121
+ display: flex;
122
+ justify-content: center;
123
+ align-items: center;
124
+ color: #BDD4FF;
125
+ cursor: pointer;
126
+ &:nth-child(2) {
127
+ margin: 0 0.6rem;
128
+ }
129
+ &:hover {
130
+ text-decoration: underline;
131
+ }
132
+ }
133
+ }
134
+ .close-button {
135
+ font-size: 12px;
136
+ padding: 0 7px;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ border-radius: 3px;
141
+ margin: 0 0.5rem;
142
+ cursor: pointer;
143
+ &::before {
144
+ content: "\2715";
145
+ }
146
+ z-index: 10;
147
+ }
148
+ }
149
+</style>
\ No newline at end of file
src/lib/Notifications/NotificationToasts.svelte
new
+24
@@ -0,0 +1,24 @@
1
+<script lang="ts">
2
+ import { toasts } from "./notifications";
3
+ import NotificationToast from "./NotificationToast.svelte";
4
+</script>
5
+<div id="notificationtoasts">
6
+ {#each $toasts as toast}
7
+ <NotificationToast id={toast.id} type={toast.type} title={toast.title} message={toast.message} actions={toast.actions} />
8
+ {/each}
9
+</div>
10
+
11
+<style lang="scss">
12
+ #notificationtoasts {
13
+ width: 25rem;
14
+ height: calc(100% - 2rem);
15
+ position: absolute;
16
+ bottom: 0;
17
+ padding: 10px 0;
18
+ right: 1rem;
19
+ box-sizing: border-box;
20
+ display: flex;
21
+ justify-content: flex-end;
22
+ flex-direction: column;
23
+ }
24
+</style>
src/lib/Notifications/notifications.ts
+31
-4
@@ -8,16 +8,23 @@ export enum NotifType {
8
class Notification {
9
id;
10
type: string;
11
+ title: string;
12
message: string;
13
read: boolean;
14
actions;
14
- constructor(message, type, read = false, actions = []) {
15
+ constructor(title, type, read = false, message = "", actions = []) {
16
+ this.title = title;
17
this.message = message;
18
this.type = getType(type);
19
this.read = read;
20
this.actions = actions;
21
}
22
}
23
+class ToastNotification extends Notification {
24
+ constructor(title, type, message = "", actions = []) {
25
+ super(title, type, false, message, actions);
26
+ }
27
+}
28
export class Action {
29
label: string;
30
action: () => void | null;
@@ -42,17 +49,30 @@ const actions = [
49
let id = 0;
50
export const unreadnotifications = writable(false);
51
export const notifications = writable([]);
45
-let notificationlist = [];
52
+export const toasts = writable([]);
53
+let notificationlist: Notification[] = [];
54
+let toastlist: ToastNotification[] = [];
55
47
-export function addNotification(type: NotifType, message: string, actions = []) {
48
- let notification = new Notification(message, type, false, actions);
56
+export function addNotification(type: NotifType, title: string, actions: Action[] = [], message = "") {
57
+ let notification = new Notification(title, type, false, message, actions);
58
+ let toast = new ToastNotification(title, type, message, actions);
59
notification.id = id;
60
+ toast.id = id;
61
notificationlist = [...notificationlist, notification];
62
+ toastlist = [...toastlist, toast];
63
notifications.set(notificationlist);
64
+ toasts.set(toastlist);
65
updateNotificationStatus();
66
id++;
67
}
68
69
+export function markAllRead() {
70
+ for (const notification of notificationlist) {
71
+ notification.read = true;
72
+ }
73
+ updateNotificationStatus();
74
+}
75
+
76
export function updateReadStatus(id, read) {
77
for (const notification of notificationlist) {
78
if (notification.id === id) {
@@ -65,7 +85,9 @@ export function updateReadStatus(id, read) {
85
86
export function clearNotifications() {
87
notificationlist = [];
88
+ toastlist = [];
89
notifications.set(notificationlist);
90
+ toasts.set(toastlist);
91
updateNotificationStatus();
92
}
93
@@ -75,6 +97,11 @@ export function closeNotification(id) {
97
updateNotificationStatus();
98
}
99
100
+export function closeToast(id) {
101
+ toastlist = toastlist.filter(n => n.id !== id);
102
+ toasts.set(toastlist);
103
+}
104
+
105
function getType(type: NotifType) {
106
switch (type) {
107
case NotifType.Message:
src/lib/Statusbar.svelte
+34
-6
@@ -3,10 +3,12 @@
3
import { line_info, language, encoding, spaces } from "./Editor.svelte";
4
import { getVersion } from '@tauri-apps/api/app';
5
import { onMount } from "svelte";
6
- import { Terminal as TerminalIcon } from "carbon-icons-svelte";
6
+ import { Terminal as TerminalIcon, Notification as NotificationIcon, TrashCan } from "carbon-icons-svelte";
7
import { invoke } from "@tauri-apps/api";
8
- import Terminal from "./Terminal.svelte";
8
+ import Terminal, { clearTerminal, closeTerminal } from "./Terminal.svelte";
9
import { workingDir } from "./File";
10
+ import NotificationList from "./Notifications/NotificationList.svelte";
11
+ import { NotifType, addNotification, clearNotifications, markAllRead, unreadnotifications } from "./Notifications/notifications";
12
13
let appVersion = "";
14
onMount(async () => {
@@ -14,7 +16,18 @@
16
})
17
18
const tools = [
17
- {name: "Terminal", content: Terminal, icon: TerminalIcon, action: (t) => {spawnTerminal(t)}}
19
+ {name: "Terminal", content: Terminal, icon: TerminalIcon, action: (t) => {spawnTerminal(t)}, options: [
20
+ {name: "Clear Terminal", action: () => {clearTerminal();}},
21
+ {name: "Close Terminal", action: () => {
22
+ closeBottomPanel();
23
+ closeTerminal();
24
+ }}
25
+ ]},
26
+ {name: "Notifications", content: NotificationList, icon: NotificationIcon, action: (t) => {toggleBottomPanel(t)}, options: [
27
+ {name: "Mark all as Read", action: () => {markAllRead()}}],
28
+ buttons: [
29
+ {icon: TrashCan, title: "Clear Notifications", action: () => {clearNotifications()}}
30
+ ]}
31
]
32
33
async function spawnTerminal(t) {
@@ -33,13 +46,13 @@
46
let currentTool = writable(null);
47
let show = false;
48
36
- export let editortool = writable({name: "", content: null});
49
+ export let editortool = writable({name: "", content: null, options: [], buttons: []});
50
51
export function toggleBottomPanel(x = null) {
52
show = !show;
53
showBottomPanel.set(show);
54
if (x) {
42
- editortool.set({name: x.name, content: x.content});
55
+ editortool.set({name: x.name, content: x.content, options: x.options, buttons: x.buttons});
56
currentTool.set(x.content);
57
}
58
}
@@ -66,8 +79,11 @@
79
<div class="editor-tools">
80
{#each tools as tool}
81
<!-- svelte-ignore a11y-click-events-have-key-events -->
69
- <span class="tool" title={tool.name} on:click={() => {tool.action(tool)}}>
82
+ <span class="tool" title={tool.name} on:click={() => {tool.action(tool)}} class:updated={$unreadnotifications}>
83
<svelte:component this={tool.icon}></svelte:component>
84
+ {#if tool.name === "Notifications"}
85
+ <span class="notification"></span>
86
+ {/if}
87
</span>
88
{/each}
89
</div>
@@ -123,6 +139,18 @@
139
&:hover {
140
cursor: pointer;
141
}
142
+ &.updated {
143
+ .notification {
144
+ position: absolute;
145
+ width: 8px;
146
+ height: 8px;
147
+ display: block;
148
+ background-color: #4589ff;
149
+ border-radius: 50%;
150
+ top: 5px;
151
+ left: 5px;
152
+ }
153
+ }
154
}
155
span {
156
display: flex;