@setoelkahfi / svara / commits / e0d57aa

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;