Refactored notification service UI

This commit is contained in:
CasVT
2025-08-01 15:54:51 +02:00
parent 767d263e45
commit 76ecb52995
5 changed files with 468 additions and 301 deletions

View File

@@ -1,15 +1,7 @@
.ns-table-container {
margin-top: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#add-service-button {
width: min(100%, 17rem);
height: 2rem;
margin-inline: auto;
display: flex;
justify-content: center;
@@ -25,129 +17,139 @@
transition: transform .125s linear;
}
.ns-table-container:has(#service-list-toggle:checked) #add-service-button > svg {
#notification:has(#service-list-toggle:checked) #add-service-button > svg {
transform: rotate(45deg);
}
.overflow-container {
#notification .entries-table {
margin-inline: auto;
width: min(100%, 50rem);
overflow-x: auto;
--inline-padding: .5rem;
}
.overflow-container > table {
border-spacing: 0px;
#services-list .empty-row td {
text-align: center;
}
.overflow-container > table:not(:has(tbody > tr)) {
#services-list:has(tr:not(.empty-row)) .empty-row {
display: none;
}
.overflow-container > table th,
.overflow-container > table td {
text-align: left;
}
.overflow-container > table th {
padding: .5rem;
}
.overflow-container td {
border-top: 1px solid var(--color-gray);
padding: .25rem;
}
.title-column {
min-width: 9.5rem;
width: 25%;
padding-left: 1.5rem;
padding-right: 1rem;
}
.url-column {
min-width: 26rem;
width: 65%;
width: 26rem;
text-wrap-mode: nowrap;
}
.overflow-container table input {
width: 100%;
border-radius: 4px;
padding: .25rem;
box-shadow: none;
}
.overflow-container input:read-only {
border-color: transparent;
}
.overflow-container .action-column {
.action-column {
min-width: 4rem;
width: 20%;
display: flex;
gap: .5rem;
padding: calc(.5rem + 2px);
padding-right: 1.5rem;
}
.action-column > button {
/* */
/* Edit and delete windows */
/* */
#edit-ns-form {
width: 100%;
height: 10rem;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1rem;
& > * {
max-width: 24rem;
&.checked-input-container > input {
max-width: inherit;
}
}
}
.overflow-container .action-column svg {
width: 1.25rem;
height: 1.25rem;
#delete-ns-dialog p {
padding-inline: 1rem;
text-align: center;
}
tr:has(input:not(:read-only)) button[data-type="save"],
tr:has(input:read-only) button[data-type="edit"] {
display: flex;
}
#confirm-delete-ns {
min-width: 6rem;
width: unset;
tr:has(input:not(:read-only)) button[data-type="edit"],
tr:has(input:read-only) button[data-type="save"] {
display: none;
padding-inline: 1rem;
}
/* */
/* Add service */
/* */
#add-service-container {
margin-top: 1rem;
display: none;
flex-direction: column;
align-items: center;
}
.overflow-container:has(#service-list-toggle:checked) table {
display: none;
#notification:has(#service-list-toggle:checked) {
& table {
display: none;
}
& #add-service-container {
display: flex;
}
}
.overflow-container:has(#service-list-toggle:checked) #add-service-container {
display: block;
#notification:has(#add-service-toggle:checked) {
& #service-list,
& #ns-search-input {
display: none;
}
& #add-service-window {
display: flex;
}
}
.overflow-container:has(#add-service-toggle:checked) #service-list {
display: none;
}
#ns-search-input {
margin-bottom: 1rem;
.overflow-container:has(#add-service-toggle:checked) #add-service-window {
display: flex;
max-width: 26rem;
height: 2rem;
padding: .25rem .5rem;
}
#service-list {
width: min(100%, 48rem);
margin-inline: auto;
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
#service-list p {
width: 100%;
text-align: center;
}
#service-list:not(:has(button)) > p,
#service-list:has(button:not(.hidden)) > p {
display: none;
}
#service-list button {
width: max(30%, 10rem);
width: 12rem;
max-width: 15rem;
height: 6rem;
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
@@ -155,15 +157,25 @@ tr:has(input:read-only) button[data-type="save"] {
padding: .75rem;
border-radius: 4px;
border: 2px solid var(--color-gray);
background-color: var(--color-dark);
color: var(--color-light);
text-align: center;
font-size: 1.1rem;
box-shadow: var(--default-shadow);
transition: background-color 150ms ease-in-out;
&:hover {
background-color: var(--color-gray);
}
}
/* */
/* Add service form */
/* */
#add-service-window {
width: 100%;
max-width: 30rem;
margin: auto;
@@ -171,129 +183,121 @@ tr:has(input:read-only) button[data-type="save"] {
flex-direction: column;
justify-content: center;
gap: 1rem;
text-align: center;
}
#add-service-window .input-style {
max-width: 100%;
&::placeholder {
color: var(--color-mid-gray);
}
}
#add-service-window > h3 {
font-size: 1.75rem;
}
#add-service-window > p {
height: 2.7rem;
margin-bottom: calc((1rem + 2px) * -1);
display: flex;
align-items: center;
border: 2px solid var(--color-gray);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: .75rem 1rem;
color: var(--color-gray);
padding-inline: .5rem;
color: var(--color-mid-gray);
text-align: left;
box-shadow: var(--default-shadow);
}
#add-service-window > button {
border-radius: 4px;
border: 2px solid var(--color-gray);
padding: .75rem;
}
#add-service-window > a,
#add-service-window > p > a {
#add-service-window > a {
color: var(--color-light);
}
#add-service-window > div[data-map],
#add-service-window > div[data-map] > .entries-list {
#add-service-window > div[data-map] {
display: flex;
flex-direction: column;
gap: inherit;
}
gap: .6rem;
#add-service-window > div[data-map] {
padding: .5rem;
border: 2px solid var(--color-gray);
border-radius: 4px;
box-shadow: var(--default-shadow);
}
#add-service-window > div[data-map] > p {
color: var(--color-gray);
font-size: 1.1rem;
& > p {
color: var(--color-mid-gray);
font-size: 1.1rem;
}
}
.entries-list {
min-height: 5rem;
max-height: 15rem;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: .6rem;
background-color: var(--color-dark);
color: var(--color-light);
border: 2px solid var(--color-gray);
border-radius: 4px;
padding: .75rem;
padding: .5rem;
box-shadow: var(--default-shadow);
font-size: 1rem;
}
.entries-list > p:first-child {
color: var(--color-gray);
color: var(--color-mid-gray);
font-size: 1.1rem;
}
.input-entries {
width: 100%;
max-height: 12rem;
width: 100%;
overflow-y: auto;
}
.input-entries:not(:has(div)) {
display: none;
}
.add-row {
width: min(100%, 21rem);
width: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
}
.add-row input {
flex-grow: 1;
height: 2rem;
min-width: 0rem;
font-size: .8rem;
}
.add-row button {
width: 6rem;
height: 2rem;
padding: .35rem .75rem;
background-color: var(--color-gray);
border-radius: 4px;
padding: 0rem;
}
.entries-list > button {
height: 1.5rem;
width: min(100%, 21rem);
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-gray);
}
height: 1.9rem;
.entries-list > button svg {
height: 60%;
}
& > svg {
height: 1rem;
.entries-list > button path {
height: inherit;
fill: var(--color-dark);
}
@media (max-width: 543px) {
#service-list button {
flex-grow: 1;
transition: transform 125ms linear;
}
}
.entries-list:has(.add-row:not(.hidden)) > button > svg {
transform: rotate(45deg);
}

View File

@@ -11,16 +11,22 @@ const constants = {
* The amount of time to wait after the user stops typing to automatically
* trigger the search
*/
autoSearchTimeout: 500
autoSearchTimeout: 500,
/**
* The amount of time to wait after the user stops typing to automatically
* trigger the search for notification services
*/
autoSearchTimeoutNs: 250
}
const icons = {
save: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path><path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path></g></svg>',
edit: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path></g></g></svg>',
delete: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path><rect x="9" y="10" width="2" height="8"></rect><rect x="13" y="10" width="2" height="8"></rect></g></g></svg>',
add: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g><path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path></g></g></svg>',
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512" height="512"><g><path d="M210.731,386.603c24.986,25.002,65.508,25.015,90.51,0.029c0.01-0.01,0.019-0.019,0.029-0.029l68.501-68.501 c7.902-8.739,7.223-22.23-1.516-30.132c-8.137-7.357-20.527-7.344-28.649,0.03l-62.421,62.443l0.149-329.109 C277.333,9.551,267.782,0,256,0l0,0c-11.782,0-21.333,9.551-21.333,21.333l-0.192,328.704L172.395,288 c-8.336-8.33-21.846-8.325-30.176,0.011c-8.33,8.336-8.325,21.846,0.011,30.176L210.731,386.603z"/><path d="M490.667,341.333L490.667,341.333c-11.782,0-21.333,9.551-21.333,21.333V448c0,11.782-9.551,21.333-21.333,21.333H64 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0C9.551,341.333,0,350.885,0,362.667V448 c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512,350.885,502.449,341.333,490.667,341.333z"/></g></svg>',
upload: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512.008 512.008" style="enable-background:new 0 0 512.008 512.008;" xml:space="preserve" width="512" height="512"><g><path d="M172.399,117.448l62.421-62.443l-0.149,329.344c0,11.782,9.551,21.333,21.333,21.333l0,0 c11.782,0,21.333-9.551,21.333-21.333l0.149-328.981l62.123,62.144c8.475,8.185,21.98,7.951,30.165-0.524 c7.985-8.267,7.985-21.374,0-29.641L301.273,18.76c-24.986-25.002-65.508-25.015-90.51-0.029c-0.01,0.01-0.019,0.019-0.029,0.029 l-68.501,68.523c-8.185,8.475-7.951,21.98,0.524,30.165C151.024,125.433,164.131,125.433,172.399,117.448z"/><path d="M490.671,341.341L490.671,341.341c-11.782,0-21.333,9.551-21.333,21.333v85.333c0,11.782-9.551,21.333-21.333,21.333h-384 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0c-11.782,0-21.333,9.551-21.333,21.333 v85.333c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512.004,350.892,502.453,341.341,490.671,341.341z"/></g></svg>',
save: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve"><g><path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path><path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path></g></svg>',
edit: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path></g></g></svg>',
delete: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path><rect x="9" y="10" width="2" height="8"></rect><rect x="13" y="10" width="2" height="8"></rect></g></g></svg>',
add: '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" xml:space="preserve"><g><g><path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path></g></g></svg>',
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" xml:space="preserve" width="512" height="512"><g><path d="M210.731,386.603c24.986,25.002,65.508,25.015,90.51,0.029c0.01-0.01,0.019-0.019,0.029-0.029l68.501-68.501 c7.902-8.739,7.223-22.23-1.516-30.132c-8.137-7.357-20.527-7.344-28.649,0.03l-62.421,62.443l0.149-329.109 C277.333,9.551,267.782,0,256,0l0,0c-11.782,0-21.333,9.551-21.333,21.333l-0.192,328.704L172.395,288 c-8.336-8.33-21.846-8.325-30.176,0.011c-8.33,8.336-8.325,21.846,0.011,30.176L210.731,386.603z"/><path d="M490.667,341.333L490.667,341.333c-11.782,0-21.333,9.551-21.333,21.333V448c0,11.782-9.551,21.333-21.333,21.333H64 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0C9.551,341.333,0,350.885,0,362.667V448 c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512,350.885,502.449,341.333,490.667,341.333z"/></g></svg>',
upload: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512.008 512.008" xml:space="preserve" width="512" height="512"><g><path d="M172.399,117.448l62.421-62.443l-0.149,329.344c0,11.782,9.551,21.333,21.333,21.333l0,0 c11.782,0,21.333-9.551,21.333-21.333l0.149-328.981l62.123,62.144c8.475,8.185,21.98,7.951,30.165-0.524 c7.985-8.267,7.985-21.374,0-29.641L301.273,18.76c-24.986-25.002-65.508-25.015-90.51-0.029c-0.01,0.01-0.019,0.019-0.029,0.029 l-68.501,68.523c-8.185,8.475-7.951,21.98,0.524,30.165C151.024,125.433,164.131,125.433,172.399,117.448z"/><path d="M490.671,341.341L490.671,341.341c-11.782,0-21.333,9.551-21.333,21.333v85.333c0,11.782-9.551,21.333-21.333,21.333h-384 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0c-11.782,0-21.333,9.551-21.333,21.333 v85.333c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512.004,350.892,502.453,341.341,490.671,341.341z"/></g></svg>',
loading: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m10.5 1.5c0-.828.672-1.5 1.5-1.5s1.5.672 1.5 1.5-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5zm1.5 22.5c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm9-12c0 .828.672 1.5 1.5 1.5s1.5-.672 1.5-1.5-.672-1.5-1.5-1.5-1.5.672-1.5 1.5zm-21 0c0 .828.672 1.5 1.5 1.5s1.5-.672 1.5-1.5-.672-1.5-1.5-1.5-1.5.672-1.5 1.5zm17.293-7.577c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm3.779 3.798c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-.01 10.567c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-3.788 3.788c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-10.577-.01c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-3.75-3.779c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-.01-10.596c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm3.779-3.769c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5z"/></svg>'
}

View File

@@ -1,5 +1,6 @@
const NotiEls = {
services_list: document.querySelector('#services-list'),
search_input: document.querySelector('#ns-search-input'),
service_list: document.querySelector('#service-list'),
default_service_input: document.querySelector('#default-service-input'),
service_selection: document.querySelector('.notification-service-selection'),
@@ -8,6 +9,27 @@ const NotiEls = {
triggers: {
add_service: document.querySelector('#add-service-toggle'),
service_list: document.querySelector('#service-list-toggle')
},
windows: {
editService: {
dialog: document.querySelector("#edit-ns-dialog"),
form: document.querySelector("#edit-ns-form"),
close: document.querySelector("#close-edit-ns"),
inputContainers: {
url: document.querySelector("#edit-ns-form .checked-input-container:has(#edit-ns-url-input)")
},
inputs: {
title: document.querySelector("#edit-ns-title-input"),
url: document.querySelector("#edit-ns-url-input")
},
error: document.querySelector("#edit-ns-url-error")
},
deleteService: {
dialog: document.querySelector("#delete-ns-dialog"),
error: document.querySelector("#delete-ns-error"),
close: document.querySelector("#close-delete-ns"),
confirm: document.querySelector("#confirm-delete-ns")
}
}
};
@@ -15,33 +37,24 @@ const NotiEls = {
// Fill lists and tables
//
function fillNotificationTable(json) {
NotiEls.services_list.innerHTML = '';
NotiEls.services_list.querySelectorAll("tr[data-id]").forEach(
e => e.remove()
)
json.result.forEach(service => {
const entry = NotiEls.notification_service_row.cloneNode(true);
entry.dataset.id = service.id;
const entry = NotiEls.notification_service_row.cloneNode(true)
entry.dataset.id = service.id
entry.querySelector('.title-column input').value = service.title;
entry.querySelector('.title-column').innerText = service.title
entry.querySelector('.url-column').innerText = service.url
const url_input = entry.querySelector('.url-column input');
url_input.value = service.url;
url_input.onkeydown = e => {
if (e.key === 'Enter')
saveService(service.id);
};
entry.querySelector('button[data-type="edit"]').onclick =
e => openEditNotificationService(service.id)
entry.querySelector('button[data-type="edit"]').onclick = e =>
document.querySelectorAll(`tr[data-id="${service.id}"] input`).forEach(
e => e.removeAttribute('readonly')
);
entry.querySelector('button[data-type="delete"]').onclick =
e => openDeleteNotificationService(service.id)
entry.querySelector('button[data-type="save"]').onclick = e =>
saveService(service.id);
entry.querySelector('button[data-type="delete"]').onclick = e =>
deleteService(service.id);
NotiEls.services_list.appendChild(entry);
});
NotiEls.services_list.appendChild(entry)
})
};
function fillNotificationSelection(json) {
@@ -58,7 +71,7 @@ function fillNotificationSelection(json) {
});
if (!NotiEls.default_service_input.querySelector(`option[value="${default_service}"]`))
setLocalStorage({'default_service':
setLocalStorage({'default_service':
parseInt(NotiEls.default_service_input.querySelector('option')?.value)
|| null
});
@@ -122,75 +135,83 @@ function fillNotificationServices() {
});
};
//
//
// Actions for table
//
function saveService(id) {
const row = document.querySelector(`tr[data-id="${id}"]`);
const save_button = row.querySelector('button[data-type="save"]');
function openEditNotificationService(serviceId) {
NotiEls.windows.editService.dialog.dataset.id = serviceId;
const row = NotiEls.services_list.querySelector(`tr[data-id="${serviceId}"]`);
NotiEls.windows.editService.inputs.title.value = row.querySelector('.title-column').innerText;
NotiEls.windows.editService.inputs.url.value = row.querySelector('.url-column').innerText;
NotiEls.windows.editService.inputContainers.url.classList.remove('error-input-container');
NotiEls.windows.editService.dialog.showModal();
};
function editNotificationService() {
NotiEls.windows.editService.inputContainers.url.classList.remove('error-input-container')
const data = {
'title': row.querySelector(`td.title-column > input`).value,
'url': row.querySelector(`td.url-column > input`).value
};
fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
title: NotiEls.windows.editService.inputs.title.value,
url: NotiEls.windows.editService.inputs.url.value
}
fillNotificationServices();
})
.catch(e => {
if (e === 401)
window.location.href = `${urlPrefix}/`;
else if (e === 400) {
save_button.classList.add('error-icon');
save_button.title = 'Invalid Apprise URL';
} else
console.log(e);
});
};
function deleteService(id, delete_reminders_using=false) {
const row = document.querySelector(`tr[data-id="${id}"]`);
fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}&delete_reminders_using=${delete_reminders_using}`, {
'method': 'DELETE'
})
.then(response => response.json())
const id = parseInt(NotiEls.windows.editService.dialog.dataset.id)
sendAPI("PUT", `/notificationservices/${id}`, {}, data)
.then(json => {
if (json.error !== null) return Promise.reject(json);
row.remove();
fillNotificationServices();
if (delete_reminders_using) {
fillLibrary(reminderTypes.reminder);
fillLibrary(reminderTypes.static_reminder);
fillLibrary(reminderTypes.template);
};
fillNotificationServices()
NotiEls.windows.editService.dialog.close()
})
.catch(e => {
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid')
window.location.href = `${urlPrefix}/`;
e.json().then(json => {
if (json.error === "URLInvalid" || json.error === "InvalidKeyValue") {
NotiEls.windows.editService.error.innerText = json.result.reason || "Syntax of URL invalid"
hide([], [NotiEls.windows.editService.error])
NotiEls.windows.editService.inputContainers.url.classList.add('error-input-container')
}
else
console.log(json)
})
})
}
else if (e.error === 'NotificationServiceInUse') {
const delete_reminders = confirm(
`The notification service is still in use by a ${e.result.reminder_type.toLowerCase()}. Do you want to delete all ${e.result.reminder_type.toLowerCase()}s that are using the notification service?`
);
if (delete_reminders)
deleteService(id, delete_reminders_using=true);
return;
function openDeleteNotificationService(serviceId) {
NotiEls.windows.deleteService.dialog.dataset.id = serviceId
NotiEls.windows.deleteService.error.classList.add("hidden")
NotiEls.windows.deleteService.confirm.innerText = "Delete"
NotiEls.windows.deleteService.confirm.onclick = e => deleteService()
NotiEls.windows.deleteService.dialog.showModal()
}
} else
console.log(e);
});
};
function deleteService(delete_reminders_using=false) {
const id = parseInt(NotiEls.windows.deleteService.dialog.dataset.id)
sendAPI("DELETE", `/notificationservices/${id}`, {delete_reminders_using: delete_reminders_using})
.then(json => {
NotiEls.windows.deleteService.error.classList.add('hidden')
fillNotificationServices()
if (delete_reminders_using) {
fillLibrary(reminderTypes.reminder)
fillLibrary(reminderTypes.static_reminder)
fillLibrary(reminderTypes.template)
}
NotiEls.windows.deleteService.dialog.close()
})
.catch(e => {
e.json().then(json => {
if (json.error === 'NotificationServiceInUse') {
NotiEls.windows.deleteService.error.innerText =
`The notification service is still in use by a ${json.result.reminder_type.toLowerCase()}. Do you want to delete all ${json.result.reminder_type.toLowerCase()}s that are using the notification service?`
NotiEls.windows.deleteService.error.classList.remove('hidden')
NotiEls.windows.deleteService.confirm.innerText = "Delete Anyway"
NotiEls.windows.deleteService.confirm.onclick = e => deleteService(delete_reminders_using=true)
}
else
console.log(json)
})
})
}
//
//
// Adding a service
//
//
function showServiceList(e) {
if (!e.target.checked)
return;
@@ -208,13 +229,51 @@ function showServiceList(e) {
const entry = document.createElement('button');
entry.innerText = result.name;
entry.onclick = e => showAddServiceWindow(index);
entry.style.viewTransitionName = `ns-${index}`;
NotiEls.service_list.appendChild(entry);
});
});
};
function searchServiceList() {
if (autoSearchTimerNs !== null)
clearTimeout(autoSearchTimerNs)
const f = () => {
const query = NotiEls.search_input.value
.toLowerCase()
.replace('-', '')
.replace('_', '')
.replace(' ', '');
if (query === '')
NotiEls.service_list.querySelectorAll('button').forEach(
e => e.classList.remove('hidden')
);
else
NotiEls.service_list.querySelectorAll('button').forEach(
e => e.classList.toggle(
'hidden',
!e.innerText
.toLowerCase()
.replace('-', '')
.replace('_', '')
.replace(' ', '')
.includes(query)
)
);
};
if (!document.startViewTransition)
f();
else
document.startViewTransition(f);
};
function createTitle() {
const service_title = document.createElement('input');
service_title.classList.add('input-style');
service_title.id = 'service-title';
service_title.type = 'text';
service_title.placeholder = 'Service Title';
@@ -224,9 +283,13 @@ function createTitle() {
function createChoice(token) {
const choice = document.createElement('select');
choice.classList.add('input-style');
choice.dataset.map = token.map_to || '';
choice.dataset.prefix = '';
choice.dataset.default = token.default || '';
if (![null, undefined, ''].includes(token.default))
choice.dataset.default = token.default
else
choice.dataset.default = ''
choice.placeholder = token.name;
choice.required = token.required;
token.options.forEach(option => {
@@ -235,7 +298,7 @@ function createChoice(token) {
entry.innerText = option;
choice.appendChild(entry);
});
if (token.default)
if (![null, undefined, ''].includes(token.default))
choice.querySelector(`option[value="${token.default}"]`).setAttribute('selected', '');
return choice;
@@ -243,6 +306,7 @@ function createChoice(token) {
function createString(token) {
const str_input = document.createElement('input');
str_input.classList.add('input-style');
str_input.dataset.map = token.map_to || '';
str_input.dataset.prefix = token.prefix || '';
str_input.dataset.regex = token.regex || '';
@@ -255,9 +319,13 @@ function createString(token) {
function createInt(token) {
const int_input = document.createElement('input');
int_input.classList.add('input-style');
int_input.dataset.map = token.map_to || '';
int_input.dataset.prefix = token.prefix || '';
int_input.dataset.default = token.default || '';
if (![null, undefined, ''].includes(token.default))
int_input.dataset.default = token.default
else
int_input.dataset.default = ''
int_input.type = 'number';
int_input.placeholder = `${token.name}${!token.required ? ' (Optional)' : ''}`;
int_input.required = token.required;
@@ -268,11 +336,35 @@ function createInt(token) {
return int_input;
};
function createFloat(token) {
const float_input = document.createElement('input');
float_input.classList.add('input-style');
float_input.dataset.map = token.map_to || '';
float_input.dataset.prefix = token.prefix || '';
if (![null, undefined, ''].includes(token.default))
float_input.dataset.default = token.default
else
float_input.dataset.default = ''
float_input.type = 'number';
float_input.step = 0.1;
float_input.placeholder = `${token.name}${!token.required ? ' (Optional)' : ''}`;
float_input.required = token.required;
if (token.min !== null)
float_input.min = token.min;
if (token.max !== null)
float_input.max = token.max;
return float_input;
};
function createBool(token) {
const bool_input = document.createElement('select');
bool_input.classList.add('input-style');
bool_input.dataset.map = token.map_to || '';
bool_input.dataset.prefix = '';
bool_input.dataset.default = token.default || '';
if (![null, undefined, ''].includes(token.default))
bool_input.dataset.default = token.default
else
bool_input.dataset.default = ''
bool_input.placeholder = token.name;
bool_input.required = token.required;
[['Yes', 'true'], ['No', 'false']].forEach(option => {
@@ -282,7 +374,7 @@ function createBool(token) {
bool_input.appendChild(entry);
});
bool_input.querySelector(`option[value="${token.default}"]`).setAttribute('selected', '');
return bool_input;
};
@@ -304,6 +396,7 @@ function createEntriesList(token) {
const add_row = document.createElement('div');
add_row.classList.add('add-row', 'hidden');
const add_input = document.createElement('input');
add_input.classList.add('input-style');
add_input.type = 'text';
add_input.onkeydown = e => {
if (e.key === "Enter") {
@@ -314,6 +407,7 @@ function createEntriesList(token) {
};
add_row.appendChild(add_input);
const add_entry_button = document.createElement('button');
add_entry_button.classList.add('input-style');
add_entry_button.type = 'button';
add_entry_button.innerText = 'Add';
add_entry_button.onclick = e => addEntry(entries_list);
@@ -321,19 +415,22 @@ function createEntriesList(token) {
entries_list.appendChild(add_row);
const add_button = document.createElement('button');
add_button.classList.add('input-style');
add_button.type = 'button';
add_button.innerHTML = icons.add;
add_button.onclick = e => toggleAddRow(add_row);
entries_list.appendChild(add_button);
return entries_list;
};
function toggleAddRow(row) {
if (row.classList.contains('hidden')) {
// Show row
row.querySelector('input').value = '';
const add_input = row.querySelector('input');
add_input.value = '';
row.classList.remove('hidden');
add_input.focus();
} else {
// Hide row
row.classList.add('hidden');
@@ -355,7 +452,7 @@ function showAddServiceWindow(index) {
const data = notification_services[index];
console.log(data);
const title = document.createElement('h3');
title.innerText = data.name;
window.appendChild(title);
@@ -366,12 +463,13 @@ function showAddServiceWindow(index) {
docs.innerText = 'Documentation';
window.appendChild(docs);
window.appendChild(createTitle());
window.appendChild(createTitle());
[[data.details.tokens, 'tokens'], [data.details.args, 'args']].forEach(vars => {
if (vars[1] === 'args' && vars[0].length > 0) {
// The args are hidden behind a "Show Advanced Settings" button
const show_args = document.createElement('button');
show_args.classList.add('input-style');
show_args.type = 'button';
show_args.innerText = 'Show Advanced Settings';
show_args.onclick = e => {
@@ -389,29 +487,31 @@ function showAddServiceWindow(index) {
desc.dataset.is_arg = vars[1] === 'args';
window.appendChild(desc);
result = createChoice(token);
} else if (token.type === 'list') {
const joint_list = document.createElement('div');
joint_list.dataset.map = token.map_to;
joint_list.dataset.delim = token.delim;
const desc = document.createElement('p');
desc.innerText = `${token.name}${!token.required ? ' (Optional)' : ''}`;
joint_list.appendChild(desc);
if (token.content.length === 0)
joint_list.appendChild(createEntriesList(token));
else
token.content.forEach(content =>
joint_list.appendChild(createEntriesList(content))
);
result = joint_list;
} else if (token.type === 'string')
result = createString(token);
else if (token.type === 'int')
result = createInt(token);
else if (token.type === 'float')
result = createFloat(token);
else if (token.type === 'bool') {
const desc = document.createElement('p');
desc.innerText = `${token.name}${!token.required ? ' (Optional)' : ''}`;
@@ -423,24 +523,26 @@ function showAddServiceWindow(index) {
result.dataset.is_arg = vars[1] === 'args';
window.appendChild(result);
});
if (vars[1] === 'args' && vars[0].length > 0)
window.querySelectorAll('[data-is_arg="true"]').forEach(
el => el.classList.toggle('hidden')
);
})
// Bottom options
const options = document.createElement('div');
options.classList.add('options');
const cancel = document.createElement('button');
cancel.classList.add('input-style');
cancel.type = 'button';
cancel.innerText = 'Cancel';
cancel.onclick = e => NotiEls.triggers.add_service.checked = false;
options.appendChild(cancel);
const test = document.createElement('button');
test.classList.add('input-style');
test.id = 'test-service';
test.type = 'button';
test.onclick = e => testService();
@@ -453,6 +555,7 @@ function showAddServiceWindow(index) {
test.appendChild(test_sent_text);
const add = document.createElement('button');
add.classList.add('input-style');
add.type = 'submit';
add.innerText = 'Add';
options.appendChild(add);
@@ -476,7 +579,7 @@ function buildAppriseURL() {
} else if (i.nodeName === 'DIV') {
let value =
[...i.querySelectorAll('.entries-list')]
.map(l =>
.map(l =>
[...l.querySelectorAll('.input-entries > div')]
.map(e => `${l.dataset.prefix || ''}${e.innerText}`)
)
@@ -493,7 +596,7 @@ function buildAppriseURL() {
const matching_templates = data.details.templates.filter(template =>
input_keys === template.replaceAll('}', '{').split('{').filter((e, i) => i % 2).sort().join()
);
if (!matching_templates.length)
return null;
@@ -509,9 +612,9 @@ function buildAppriseURL() {
if (['INPUT', 'SELECT'].includes(el.nodeName) && el.value && el.value !== el.dataset.default)
return `${el.dataset.map}=${el.value}`;
else if (el.nodeName == 'DIV') {
let value =
let value =
[...el.querySelectorAll('.entries-list')]
.map(l =>
.map(l =>
[...l.querySelectorAll('.input-entries > div')]
.map(e => `${l.dataset.prefix || ''}${e.innerText}`)
)
@@ -575,7 +678,7 @@ function testService() {
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
test_button.classList.remove('error-input');
test_button.title = '';
test_button.classList.add('show-sent');
@@ -593,14 +696,14 @@ function testService() {
function addService() {
const add_button = NotiEls.add_service_window.querySelector('.options > button[type="submit"]');
// Check regexes for input's
[...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')]
.forEach(el => el.classList.remove('error-input'));
const faulty_inputs =
[...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')]
.filter(el =>
.filter(el =>
!(
(!el.required && el.value === '')
||
@@ -631,7 +734,7 @@ function addService() {
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
add_button.classList.remove('error-input');
add_button.title = '';
@@ -657,4 +760,29 @@ fillNotificationServices();
let notification_services = null;
NotiEls.triggers.service_list.onchange = showServiceList;
NotiEls.add_service_window.action = 'javascript:addService();';
NotiEls.add_service_window.action = 'javascript:addService();';
NotiEls.windows.editService.dialog.onclick = e => {
if (e.target === e.currentTarget) {
e.stopPropagation()
NotiEls.windows.editService.dialog.close();
}
}
NotiEls.windows.editService.form.action = 'javascript:editNotificationService()'
NotiEls.windows.editService.close.onclick = e => NotiEls.windows.editService.dialog.close();
NotiEls.windows.deleteService.dialog.onclick = e => {
if (e.target === e.currentTarget) {
e.stopPropagation()
NotiEls.windows.deleteService.dialog.close()
}
}
NotiEls.windows.deleteService.close.onclick = e => NotiEls.windows.deleteService.dialog.close()
var autoSearchTimerNs = null
NotiEls.search_input.oninput = e => {
if (autoSearchTimerNs !== null)
clearTimeout(autoSearchTimerNs)
autoSearchTimerNs = setTimeout(searchServiceList, constants.autoSearchTimeoutNs)
}

View File

@@ -49,7 +49,21 @@ function showWindow(id) {
constants.windowAnimationDuration
)
document.body.onkeydown = null
if (id === "notification") {
document.body.onkeydown = e => {
if (
e.key === '/'
&& NotiEls.triggers.service_list.checked
&& !NotiEls.triggers.add_service.checked
&& document.activeElement !== NotiEls.search_input
) {
NotiEls.search_input.focus()
e.preventDefault()
}
}
}
else
document.body.onkeydown = null
}
}

View File

@@ -38,12 +38,8 @@
<table>
<tbody>
<tr class="notification-service-row" data-id="">
<td class="title-column">
<input type="text" readonly>
</td>
<td class="url-column">
<input type="text" readonly>
</td>
<td class="title-column"></td>
<td class="url-column"></td>
<td class="action-column">
<button data-type="edit" title="Edit">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
@@ -54,14 +50,6 @@
</g>
</svg>
</button>
<button data-type="save" title="Save Edits">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
<g>
<path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path>
<path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path>
</g>
</svg>
</button>
<button data-type="delete" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">
<g>
@@ -91,6 +79,41 @@
</div>
</dialog>
<dialog id="edit-ns-dialog">
<div class="dialog-container">
<div class="dialog-header">
<h2>Edit Notification Service</h2>
</div>
<div class="dialog-content">
<form id="edit-ns-form">
<input type="text" id="edit-ns-title-input" placeholder="Title" class="input-style" required>
<div class="checked-input-container">
<input type="text" id="edit-ns-url-input" placeholder="Apprise URL" class="input-style" required>
<p id="edit-ns-url-error">Invalid URL</p>
</div>
</form>
</div>
<div class="dialog-footer">
<button id="close-edit-ns" class="input-style">Cancel</button>
<button type="submit" form="edit-ns-form" class="input-style">Save</button>
</div>
</div>
</dialog>
<dialog id="delete-ns-dialog">
<div class="dialog-container">
<div class="dialog-content">
<p>Are you sure you want to permanently delete this notification service?</p>
<p class="error hidden" id="delete-ns-error">Failed to delete notification service</p>
<div class="confirm-container">
<button id="confirm-delete-ns" class="input-style">Delete</button>
<button id="close-delete-ns" class="input-style">Cancel</button>
</div>
</div>
</div>
</dialog>
<header>
<div>
<label for="nav-toggle" id="toggle-nav">
@@ -341,47 +364,39 @@
<div id="notification">
<h2>Notification Services</h2>
<p>Setup your notification providers here</p>
<div class="ns-table-container">
<label
id="add-service-button"
title="Toggle adding notification service"
for="service-list-toggle"
>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<label id="add-service-button" for="service-list-toggle" title="Toggle adding notification service">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" xml:space="preserve">
<g>
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</svg>
</label>
<div class="overflow-container">
<input type="checkbox" id="service-list-toggle" class="hidden">
<input type="checkbox" id="add-service-toggle" class="hidden">
<table>
<thead>
<tr>
<th class="title-column">Title</th>
<th class="url-column">Apprise URL</th>
<th title="Actions" aria-label="Actions" class="action-column">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M11.24,24a2.262,2.262,0,0,1-.948-.212,2.18,2.18,0,0,1-1.2-2.622L10.653,16H6.975A3,3,0,0,1,4.1,12.131l3.024-10A2.983,2.983,0,0,1,10,0h3.693a2.6,2.6,0,0,1,2.433,3.511L14.443,8H17a3,3,0,0,1,2.483,4.684l-6.4,10.3A2.2,2.2,0,0,1,11.24,24ZM10,2a1,1,0,0,0-.958.71l-3.024,10A1,1,0,0,0,6.975,14H12a1,1,0,0,1,.957,1.29L11.01,21.732a.183.183,0,0,0,.121.241A.188.188,0,0,0,11.4,21.9l6.4-10.3a1,1,0,0,0,.078-1.063A.979.979,0,0,0,17,10H13a1,1,0,0,1-.937-1.351l2.19-5.84A.6.6,0,0,0,13.693,2Z"></path>
</g>
</svg>
</th>
</tr>
</thead>
<tbody id="services-list">
</tbody>
</table>
<div id="add-service-container">
<div id="service-list">
</div>
<form id="add-service-window"></form>
</div>
</g>
</svg>
</label>
<input type="checkbox" id="service-list-toggle" class="hidden">
<input type="checkbox" id="add-service-toggle" class="hidden">
<div class="table-container">
<table class="entries-table">
<thead>
<tr>
<th class="title-column">Title</th>
<th class="url-column">Apprise URL</th>
<th title="Actions">Actions</th>
</tr>
</thead>
<tbody id="services-list">
<tr class="empty-row">
<td colspan="3">No Services</td>
</tr>
</tbody>
</table>
</div>
<div id="add-service-container">
<input type="text" id="ns-search-input" class="input-style" placeholder="Type '/' to search services...">
<div id="service-list">
<p>No Results</p>
</div>
<form id="add-service-window"></form>
</div>
</div>