Compare commits

..

47 Commits

Author SHA1 Message Date
cdcb02e976 commit 18/10/2025 sync changes from master 2025-10-20 08:53:20 +07:00
17b4485e69 commit 17/10/2025 Add few logs 2025-10-17 17:15:00 +07:00
4d02ab6d07 commit 17/10/2025 Add few logs 2025-10-17 16:55:57 +07:00
8bd57f216a commit 14/10/2025 WebApp fix ScheduleBank ADD & send message 2025-10-17 09:42:50 +07:00
8978a0986d Merge remote-tracking branch 'origin/master' into feature-webapp 2025-10-17 09:12:01 +07:00
bc3f5c5691 commit 16/10/2025
Overview menu beres
2025-10-16 15:54:33 +07:00
e5d8d8059e Merge branch 'master' of https://gitea.rdkartono.my.id/rdkartono/AAS_NewGeneration into feature-webapp 2025-10-16 15:42:24 +07:00
1b84ec133b commit 14/10/2025 WebApp Add send language for ScheduleBank 2025-10-16 15:26:59 +07:00
1a6b7de6ec commit 16/10/2025
Overview menu beres
2025-10-16 14:53:26 +07:00
4da5a2fb05 Merge branch 'master' of https://gitea.rdkartono.my.id/rdkartono/AAS_NewGeneration into feature-webapp 2025-10-15 16:17:05 +07:00
2ca7004b70 commit 15/10/2025
Overview menu belum beres
2025-10-15 16:13:44 +07:00
1563e233d6 commit 14/10/2025 WebApp Add send language for ScheduleBank 2025-10-15 13:32:08 +07:00
2fe4a46e3e commit 15/10/2025
Network monitoring beres
2025-10-15 12:14:57 +07:00
a53270aaed commit 14/10/2025 WebApp Add send language for ScheduleBank 2025-10-14 16:26:14 +07:00
4d3dc538bd commit 14/10/2025
Messagebank belum beres
2025-10-14 15:14:21 +07:00
d2a2626fd5 Merge remote-tracking branch 'origin/master' into feature-webapp 2025-10-14 14:20:08 +07:00
de54d142ae commit 14/10/2025
Soundbank Menu beres
2025-10-14 13:22:48 +07:00
8c49bb827f Merge remote-tracking branch 'origin/master' into feature-webapp
# Conflicts:
#	src/web/WebApp.kt
2025-10-14 09:11:36 +07:00
b15470845e Merge remote-tracking branch 'origin/master' into feature-webapp
# Conflicts:
#	src/web/WebApp.kt
2025-10-14 09:07:17 +07:00
5f57e1bf2e commit 14/10/2025
combine with coding steph
2025-10-14 08:50:05 +07:00
a133f9a170 commit 14/10/2025
combine with coding steph
2025-10-14 08:21:07 +07:00
1d2c8d1307 commit 08/10/2025 WebApp 2025-10-14 07:21:52 +07:00
e426522380 commit 08/10/2025 WebApp 2025-10-13 16:16:15 +07:00
110e6d5b12 commit 13/10/2025
Soundbank menu
2025-10-13 15:55:09 +07:00
470f61c79d Merge branch 'master' of https://gitea.rdkartono.my.id/rdkartono/AAS_NewGeneration into feature-webapp
# Conflicts:
#	src/web/WebApp.kt
2025-10-13 10:52:21 +07:00
e78fb932b2 commit 10/10/2025
Restrukturisasi soundbank path di database
2025-10-10 16:21:28 +07:00
e0cdf74dec commit 10/10/2025
Restrukturisasi soundbank path di database
2025-10-10 16:21:13 +07:00
41d6dd7f47 commit 10/10/2025
Broadcast Zones and Sound Channels
2025-10-10 15:01:09 +07:00
fdc7556dd7 commit 10/10/2025
User Management
2025-10-10 10:03:14 +07:00
7f647fe9c3 commit 10/10/2025
User Management
2025-10-10 09:52:11 +07:00
d549aee42c commit 09/10/2025
User Management belum kelar
2025-10-09 15:49:03 +07:00
6b00bc7eb0 commit 08/10/2025 WebApp 2025-10-08 17:03:17 +07:00
8409307631 Merge branch 'master' of https://gitea.rdkartono.my.id/rdkartono/AAS_NewGeneration into feature-webapp 2025-10-08 17:02:45 +07:00
2ad26c3ef6 commit 08/10/2025
BarixConnection Activate Deactivate Relays
2025-10-08 16:33:19 +07:00
c0e920d1d5 commit 08/10/2025 WebApp 2025-10-08 14:32:09 +07:00
0d6b4aa49e Merge remote-tracking branch 'origin/master' into feature-webapp 2025-10-08 14:25:10 +07:00
efe243e440 commit 07/10/2025 2025-10-08 14:10:50 +07:00
86d50bdb6c commit 08/10/2025 WebApp 2025-10-08 10:28:15 +07:00
1318bba397 commit 07/10/2025 2025-10-07 15:59:36 +07:00
e8695c7a6f commit 07/10/2025 2025-10-07 15:57:23 +07:00
d04a8bedd1 commit 07/10/2025 2025-10-07 14:18:00 +07:00
1c72c7577f commit 07/10/2025 2025-10-07 13:30:27 +07:00
748301a5cb commit 07/10/2025 2025-10-07 09:28:48 +07:00
a8e5b027ef commit 06/10/2025 2025-10-06 16:00:43 +07:00
611745439f commit 06/10/2025 2025-10-06 13:50:00 +07:00
cfb38556b5 commit 06/10/2025 2025-10-06 11:14:35 +07:00
13a45b706b commit 06/10/2025 2025-10-06 11:06:32 +07:00
74 changed files with 5727 additions and 1841 deletions

6
.gitignore vendored
View File

@@ -30,3 +30,9 @@ bin/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
## Soundbank directories ##
PagingResult/
SoundBank/
SoundbankResult/
logs/

View File

@@ -1,11 +1,11 @@
<component name="libraryTable"> <component name="libraryTable">
<library name="github.oshi.core" type="repository"> <library name="github.oshi.core" type="repository">
<properties maven-id="com.github.oshi:oshi-core:6.6.2" /> <properties maven-id="com.github.oshi:oshi-core:6.9.0" />
<CLASSES> <CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/github/oshi/oshi-core/6.6.2/oshi-core-6.6.2.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/com/github/oshi/oshi-core/6.9.0/oshi-core-6.9.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.17.0/jna-5.17.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna-platform/5.14.0/jna-platform-5.14.0.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna-platform/5.17.0/jna-platform-5.17.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.13/slf4j-api-2.0.13.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar!/" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />

View File

@@ -1,8 +1,8 @@
<component name="libraryTable"> <component name="libraryTable">
<library name="net.java.dev.jna" type="repository"> <library name="net.java.dev.jna" type="repository">
<properties maven-id="net.java.dev.jna:jna:5.17.0" /> <properties maven-id="net.java.dev.jna:jna:5.18.1" />
<CLASSES> <CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.17.0/jna-5.17.0.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.18.1/jna-5.18.1.jar!/" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />

View File

@@ -1,9 +1,9 @@
<component name="libraryTable"> <component name="libraryTable">
<library name="tinylog.impl" type="repository"> <library name="tinylog.impl" type="repository">
<properties maven-id="org.tinylog:tinylog-impl:2.6.2" /> <properties maven-id="org.tinylog:tinylog-impl:2.7.0" />
<CLASSES> <CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/tinylog/tinylog-impl/2.6.2/tinylog-impl-2.6.2.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/org/tinylog/tinylog-impl/2.7.0/tinylog-impl-2.7.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/tinylog/tinylog-api/2.6.2/tinylog-api-2.6.2.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/org/tinylog/tinylog-api/2.7.0/tinylog-api-2.7.0.jar!/" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />

View File

@@ -9,6 +9,7 @@
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/audiofiles" isTestSource="false" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

BIN
audiofiles/chimedown.wav Normal file

Binary file not shown.

BIN
audiofiles/chimeup.wav Normal file

Binary file not shown.

BIN
audiofiles/silence1s.wav Normal file

Binary file not shown.

BIN
audiofiles/silencehalf.wav Normal file

Binary file not shown.

View File

@@ -0,0 +1,88 @@
:root, [data-bs-theme=light] {
--bs-primary: #0d6efd;
--bs-primary-rgb: 13,110,253;
--bs-primary-text-emphasis: #052C65;
--bs-primary-bg-subtle: #CFE2FF;
--bs-primary-border-subtle: #9EC5FE;
}
.btn-primary {
--bs-btn-color: #fff;
--bs-btn-bg: #0d6efd;
--bs-btn-border-color: #0d6efd;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #0B5ED7;
--bs-btn-hover-border-color: #0A58CA;
--bs-btn-focus-shadow-rgb: 219,233,255;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #0A58CA;
--bs-btn-active-border-color: #0A53BE;
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #0d6efd;
--bs-btn-disabled-border-color: #0d6efd;
}
.btn-outline-primary {
--bs-btn-color: #0d6efd;
--bs-btn-border-color: #0d6efd;
--bs-btn-focus-shadow-rgb: 13,110,253;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #0d6efd;
--bs-btn-hover-border-color: #0d6efd;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #0d6efd;
--bs-btn-active-border-color: #0d6efd;
--bs-btn-disabled-color: #0d6efd;
--bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #0d6efd;
}
.my-4 {
margin-top: 1.5rem!important;
margin-bottom: 1.5rem!important;
}
.mt-0 {
margin-top: 0!important;
}
.me-2 {
margin-right: .5rem!important;
}
.mb-2 {
margin-bottom: .5rem!important;
}
.mb-3 {
margin-bottom: 1rem!important;
}
.mb-4 {
margin-bottom: 1.5rem!important;
}
.mb-5 {
margin-bottom: 3rem!important;
}
.mb-7 {
margin-bottom: 6rem !important;
}
.mb-auto {
margin-bottom: auto!important;
}
@media (min-width:768px) {
.me-md-auto {
margin-right: auto!important;
}
}
@media (min-width:768px) {
.mb-md-0 {
margin-bottom: 0!important;
}
}

629
html/webpage/assets/css/select2.min.css vendored Normal file
View File

@@ -0,0 +1,629 @@
.select2-container {
box-sizing: border-box;
display: inline-block;
margin: 0;
position: relative;
vertical-align: middle;
}
.select2-container .select2-selection--single {
box-sizing: border-box;
cursor: pointer;
display: block;
height: 28px;
user-select: none;
-webkit-user-select: none;
}
.select2-container .select2-selection--single .select2-selection__rendered {
display: block;
padding-left: 8px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select2-container .select2-selection--single .select2-selection__clear {
position: relative;
}
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 8px;
padding-left: 20px;
}
.select2-container .select2-selection--multiple {
box-sizing: border-box;
cursor: pointer;
display: block;
min-height: 32px;
user-select: none;
-webkit-user-select: none;
}
.select2-container .select2-selection--multiple .select2-selection__rendered {
display: inline-block;
overflow: hidden;
padding-left: 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
.select2-container .select2-search--inline {
float: left;
}
.select2-container .select2-search--inline .select2-search__field {
box-sizing: border-box;
border: none;
font-size: 100%;
margin-top: 5px;
padding: 0;
}
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none;
}
.select2-dropdown {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
box-sizing: border-box;
display: block;
position: absolute;
left: -100000px;
width: 100%;
z-index: 1051;
}
.select2-results {
display: block;
}
.select2-results__options {
list-style: none;
margin: 0;
padding: 0;
}
.select2-results__option {
padding: 6px;
user-select: none;
-webkit-user-select: none;
}
.select2-results__option[aria-selected] {
cursor: pointer;
}
.select2-container--open .select2-dropdown {
left: 0;
}
.select2-container--open .select2-dropdown--above {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--open .select2-dropdown--below {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-search--dropdown {
display: block;
padding: 4px;
}
.select2-search--dropdown .select2-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box;
}
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none;
}
.select2-search--dropdown.select2-search--hide {
display: none;
}
.select2-close-mask {
border: 0;
margin: 0;
padding: 0;
display: block;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 99;
background-color: #fff;
filter: alpha(opacity=0);
}
.select2-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}
.select2-container--default .select2-selection--single {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px;
}
.select2-container--default .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #999;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--default.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default;
}
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--default .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
}
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%;
}
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--default .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-top: 5px;
margin-right: 10px;
padding: 1px;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333;
}
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: solid black 1px;
outline: 0;
}
.select2-container--default.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default;
}
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
}
.select2-container--default .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--default .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
}
.select2-container--default .select2-results__option[role=group] {
padding: 0;
}
.select2-container--default .select2-results__option[aria-disabled=true] {
color: #999;
}
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #ddd;
}
.select2-container--default .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #5897fb;
color: white;
}
.select2-container--default .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}
.select2-container--classic .select2-selection--single {
background-color: #f7f7f7;
border: 1px solid #aaa;
border-radius: 4px;
outline: 0;
background-image: -webkit-linear-gradient(top, #fff 50%, #eee 100%);
background-image: -o-linear-gradient(top, #fff 50%, #eee 100%);
background-image: linear-gradient(to bottom, #fff 50%, #eee 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0);
}
.select2-container--classic .select2-selection--single:focus {
border: 1px solid #5897fb;
}
.select2-container--classic .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px;
}
.select2-container--classic .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px;
}
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: #999;
}
.select2-container--classic .select2-selection--single .select2-selection__arrow {
background-color: #ddd;
border: none;
border-left: 1px solid #aaa;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
background-image: -webkit-linear-gradient(top, #eee 50%, #ccc 100%);
background-image: -o-linear-gradient(top, #eee 50%, #ccc 100%);
background-image: linear-gradient(to bottom, #eee 50%, #ccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0);
}
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
border: none;
border-right: 1px solid #aaa;
border-radius: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 1px;
right: auto;
}
.select2-container--classic.select2-container--open .select2-selection--single {
border: 1px solid #5897fb;
}
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
background: transparent;
border: none;
}
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-image: -webkit-linear-gradient(top, #fff 0%, #eee 50%);
background-image: -o-linear-gradient(top, #fff 0%, #eee 50%);
background-image: linear-gradient(to bottom, #fff 0%, #eee 50%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0);
}
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-image: -webkit-linear-gradient(top, #eee 50%, #fff 100%);
background-image: -o-linear-gradient(top, #eee 50%, #fff 100%);
background-image: linear-gradient(to bottom, #eee 50%, #fff 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0);
}
.select2-container--classic .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
outline: 0;
}
.select2-container--classic .select2-selection--multiple:focus {
border: 1px solid #5897fb;
}
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
list-style: none;
margin: 0;
padding: 0 5px;
}
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
display: none;
}
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
color: #888;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #555;
}
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
float: right;
margin-left: 5px;
margin-right: auto;
}
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--classic.select2-container--open .select2-selection--multiple {
border: 1px solid #5897fb;
}
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--classic .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
outline: 0;
}
.select2-container--classic .select2-search--inline .select2-search__field {
outline: 0;
box-shadow: none;
}
.select2-container--classic .select2-dropdown {
background-color: #fff;
border: 1px solid transparent;
}
.select2-container--classic .select2-dropdown--above {
border-bottom: none;
}
.select2-container--classic .select2-dropdown--below {
border-top: none;
}
.select2-container--classic .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
}
.select2-container--classic .select2-results__option[role=group] {
padding: 0;
}
.select2-container--classic .select2-results__option[aria-disabled=true] {
color: grey;
}
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
background-color: #3875d7;
color: #fff;
}
.select2-container--classic .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}
.select2-container--classic.select2-container--open .select2-dropdown {
border-color: #5897fb;
}

View File

@@ -62,9 +62,9 @@ body {
.btn-round-basic:focus { .btn-round-basic:focus {
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 20px; border-radius: 8px;
box-shadow: inset 4px 4px 10px #88a5bf7b, inset -4px -4px 10px #ffffff; box-shadow: inset 4px 4px 10px #88a5bf7b, inset -4px -4px 10px #ffffff;
color: #4d4d4d; /*color: #4d4d4d;*/
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
@@ -72,7 +72,7 @@ body {
} }
.btn-round-basic { .btn-round-basic {
border-radius: 20px; border-radius: 08px;
box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px; box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px;
--bs-btn-hover-bg: #ffffff; --bs-btn-hover-bg: #ffffff;
} }
@@ -82,29 +82,29 @@ body {
box-shadow: inset 2px 2px 5px #bcbcbc, inset -2px -2px 5px #ffffff, 2px 2px 5px #bcbcbc, -2px -2px 5px #ffffff; box-shadow: inset 2px 2px 5px #bcbcbc, inset -2px -2px 5px #ffffff, 2px 2px 5px #bcbcbc, -2px -2px 5px #ffffff;
} }
.color-import { .color-import, .color-import:hover, .color-import:focus {
color: var(--bs-teal); color: var(--bs-teal);
} }
.color-remove { .color-remove, .color-remove:hover, .color-remove:focus {
color: var(--bs-danger); color: var(--bs-danger);
} }
.color-edit { .color-edit, .color-edit:hover, .color-edit:focus {
color: var(--bs-primary-text-emphasis); color: var(--bs-primary-text-emphasis);
} }
.color-add { .color-add, .color-add:hover, .color-add:focus {
color: var(--bs-primary); color: var(--bs-primary);
} }
.input-login { .input-login {
border-radius: 50px; border-radius: 8px;
box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px; box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px;
} }
.btn-login { .btn-login {
border-radius: 20px; border-radius: 8px;
box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px; box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px;
--bs-btn-hover-bg: #5780f2; --bs-btn-hover-bg: #5780f2;
background-color: #5278e1; background-color: #5278e1;
@@ -227,25 +227,23 @@ nav-item:focus {
} }
.accordion-item.active { .accordion-item.active {
/*background: rgba(45, 53, 120, 0.15);*/ background: rgba(45, 53, 120, 0.15);
/*box-shadow: inset 4px 4px 10px rgba(45, 53, 120, 0.25), inset -4px -4px 10px rgba(255, 255, 255, 0.8);*/ box-shadow: inset 4px 4px 10px rgba(45, 53, 120, 0.25), inset -4px -4px 10px rgba(255, 255, 255, 0.8);
} }
.bg-accordion { .bg-accordion {
border: white 2px; border: white 2px;
/*border-radius: 10px;*/ border-radius: 8px;
background: #f8f9fd; background: #f8f9fd;
} }
.card-channel { .card-channel {
border-radius: 12px; border-radius: 8px;
background: #ffffff; background: #ffffff;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
border: #ffffff solid 2px !important; border: #ffffff solid 2px !important;
/*padding-left: 2px;*/
/*padding-right: 5px;*/
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
} }
@@ -255,3 +253,68 @@ nav-item:focus {
border: white solid 3px; border: white solid 3px;
} }
.progress-bar {
background-color: #172066;
}
table {
border-radius: 8px;
}
.bg-modal-body {
background-color: #f8f9fd;
}
.bg-header-modal {
background-color: #f0f2ff;
}
.pad-row-btn {
margin: 0.5rem;
}
.pad-2 {
border-radius: 0 !important;
}
.p-login {
text-align: left;
font-weight: 600;
margin-bottom: 0;
color: #3E4C66;
}
.h-login {
font-weight: 600;
color: #2E3A59;
}
#file-input {
display: none;
}
.card-setting {
border-radius: 8px;
border: white solid 3px;
}
#drop-area {
border: 2px dashed #ccc;
border-radius: 20px;
width: 400px;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
color: #666;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
#drop-area.highlight {
background: #f0f8ff;
border-color: #0d6efd;
color: #0d6efd;
}

View File

@@ -1,4 +1,17 @@
/**
* @typedef {Object} BroadcastZone
* @property {number} index
* @property {string} description
* @property {String} SoundChannel
* @property {String} Box
* @property {String} Relay
*/
/**
* List of broadcast zones available
* @type {BroadcastZone[]}
*/
window.BroadcastZoneList ??= [];
/** /**
* Currently selected broadcast zone row in the table * Currently selected broadcast zone row in the table
@@ -6,6 +19,12 @@
*/ */
window.selectedBroadcastZoneRow = null; window.selectedBroadcastZoneRow = null;
/**
* List of sound channels available
* @type {String[]}
*/
window.SoundChannelList = []
/** /**
* Fill broadcast zone table body with values * Fill broadcast zone table body with values
* @param {BroadcastZone[]} vv values to fill * @param {BroadcastZone[]} vv values to fill
@@ -23,7 +42,7 @@ function fill_broadcastzonetablebody(vv) {
</tr>`; </tr>`;
$('#broadcastzonetablebody').append(row); $('#broadcastzonetablebody').append(row);
let $addedrow = $('#broadcastzonetablebody tr:last'); let $addedrow = $('#broadcastzonetablebody tr:last');
$addedrow.click(function () { $addedrow.off('click').on('click', function () {
if (window.selectedBroadcastZoneRow) { if (window.selectedBroadcastZoneRow) {
window.selectedBroadcastZoneRow.find('td').css('background-color', ''); window.selectedBroadcastZoneRow.find('td').css('background-color', '');
if (window.selectedBroadcastZoneRow.is($(this))) { if (window.selectedBroadcastZoneRow.is($(this))) {
@@ -42,7 +61,34 @@ function fill_broadcastzonetablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length); $('#tablesize').text("Table Size: " + vv.length);
} }
/**
* Reload broadcast zones from server
* @param {String} APIURL API URL endpoint (default "BroadcastZones/")
*/
function reloadBroadcastZones(APIURL = "BroadcastZones/") {
window.BroadcastZoneList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadBroadcastZones : ", okdata)
window.BroadcastZoneList.push(...okdata);
fill_broadcastzonetablebody(window.BroadcastZoneList);
} else console.log("reloadBroadcastZones: okdata is not array");
}, (errdata) => {
alert("Error loading broadcast zones : " + errdata.message);
});
}
function fetchSoundChannels(APIURL = "SoundChannel/") {
window.SoundChannelList = [];
fetchAPI(APIURL + "SoundChannelDescriptions", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("fetchSoundChannels : ", okdata)
window.SoundChannelList.push(...okdata);
} else console.log("fetchSoundChannels: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
$(document).ready(function () { $(document).ready(function () {
console.log("broadcastzones.js loaded successfully"); console.log("broadcastzones.js loaded successfully");
@@ -67,7 +113,7 @@ $(document).ready(function () {
let $findzone = $('#findzone'); let $findzone = $('#findzone');
$findzone.on('input', function () { $findzone.off('input').on('input', function () {
let searchTerm = $findzone.val().trim().toLowerCase(); let searchTerm = $findzone.val().trim().toLowerCase();
if (searchTerm.length > 0) { if (searchTerm.length > 0) {
window.selectedBroadcastZoneRow = null; window.selectedBroadcastZoneRow = null;
@@ -97,36 +143,35 @@ $(document).ready(function () {
$broadcastzonedescription.val(''); $broadcastzonedescription.val('');
// fill broadcastzonesoundchannel from SoundChannelList // fill broadcastzonesoundchannel from SoundChannelList
$broadcastzonesoundchannel.empty(); $broadcastzonesoundchannel.empty();
if (Array.isArray(SoundChannelList) && SoundChannelList.length > 0) { console.log("SoundChannelList:", window.SoundChannelList);
if (Array.isArray(window.SoundChannelList) && window.SoundChannelList.length > 0) {
// SoundChannelList ada isinya // SoundChannelList ada isinya
SoundChannelList.forEach(ch => { window.SoundChannelList.forEach(ch => {
if (ch.channel && ch.channel.length > 0){ if (ch && ch.length>0){
// hanya yang punya channel saja $broadcastzonesoundchannel.append($('<option>').val(ch).text(ch));
$broadcastzonesoundchannel.append($('<option>').val(ch.channel).text(ch.channel));
} }
}); });
} }
$broadcastzonebox.val(''); $broadcastzonebox.val('1').prop('disabled', true);
for (let i = 1; i <= 32; i++) { for (let i = 1; i <= 32; i++) {
cbRelay(i).prop('checked', false); cbRelay(i).prop('checked', false);
} }
} }
reloadBroadcastZones(APIURL_BroadcastZone, () => { fetchSoundChannels();
fill_broadcastzonetablebody(window.BroadcastZoneList); reloadBroadcastZones(APIURL_BroadcastZone);
});
$btnClear.click(() => { $btnClear.off('click').on('click', () => {
DoClear(APIURL_BroadcastZone, "BroadcastZones", (okdata) => { DoClear(APIURL_BroadcastZone, "BroadcastZones", (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone); reloadBroadcastZones(APIURL_BroadcastZone);
fill_broadcastzonetablebody(window.BroadcastZoneList);
alert("Success clear broadcast zones: " + okdata.message); alert("Success clear broadcast zones: " + okdata.message);
}, (errdata) => { }, (errdata) => {
alert("Error clear broadcast zones: " + errdata.message); alert("Error clear broadcast zones: " + errdata.message);
}); });
}); });
$btnAdd.click(() => { $btnAdd.off('click').on('click', () => {
$broadcastzonemodal.modal('show'); $broadcastzonemodal.modal('show');
clearBroadcastZoneModal(); clearBroadcastZoneModal();
@@ -165,10 +210,8 @@ $(document).ready(function () {
Relay: relay Relay: relay
}; };
fetchAPI(APIURL_BroadcastZone + "Add", "POST", {}, bz, (okdata) => { fetchAPI(APIURL_BroadcastZone + "Add", "POST", {}, bz, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone, () => { reloadBroadcastZones(APIURL_BroadcastZone);
fill_broadcastzonetablebody(window.BroadcastZoneList); alert("Success add new broadcast zone: " + okdata.message);
alert("Success add new broadcast zone: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error add new broadcast zone: " + errdata.message); alert("Error add new broadcast zone: " + errdata.message);
}); });
@@ -180,12 +223,12 @@ $(document).ready(function () {
}); });
}); });
$btnRemove.click(() => { $btnRemove.off('click').on('click', () => {
if (window.selectedBroadcastZoneRow) { if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td'); let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */ /** @type {BroadcastZone} */
let bz = { let bz = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
SoundChannel: cells.eq(2).text(), SoundChannel: cells.eq(2).text(),
Box: cells.eq(3).text(), Box: cells.eq(3).text(),
@@ -193,10 +236,8 @@ $(document).ready(function () {
}; };
if (confirm(`Are you sure to delete broadcast zone [${bz.index}] Description=${bz.description}?`)) { if (confirm(`Are you sure to delete broadcast zone [${bz.index}] Description=${bz.description}?`)) {
fetchAPI(APIURL_BroadcastZone + "DeleteByIndex/" + bz.index, "DELETE", {}, null, (okdata) => { fetchAPI(APIURL_BroadcastZone + "DeleteByIndex/" + bz.index, "DELETE", {}, null, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone, () => { reloadBroadcastZones(APIURL_BroadcastZone);
fill_broadcastzonetablebody(window.BroadcastZoneList); alert("Success delete broadcast zone: " + okdata.message);
alert("Success delete broadcast zone: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error delete broadcast zone: " + errdata.message); alert("Error delete broadcast zone: " + errdata.message);
}); });
@@ -204,12 +245,12 @@ $(document).ready(function () {
} }
}); });
$btnEdit.click(() => { $btnEdit.off('click').on('click', () => {
if (window.selectedBroadcastZoneRow) { if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td'); let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */ /** @type {BroadcastZone} */
let bz = { let bz = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
SoundChannel: cells.eq(2).text(), SoundChannel: cells.eq(2).text(),
Box: cells.eq(3).text(), Box: cells.eq(3).text(),
@@ -221,12 +262,12 @@ $(document).ready(function () {
$broadcastzoneindex.val(bz.index); $broadcastzoneindex.val(bz.index);
$broadcastzonedescription.val(bz.description); $broadcastzonedescription.val(bz.description);
$broadcastzonesoundchannel.val(bz.SoundChannel); $broadcastzonesoundchannel.val(bz.SoundChannel);
$broadcastzonebox.val(bz.id); $broadcastzonebox.val(bz.Box);
if (bz.bp) { if (bz.Relay && bz.Relay.length > 0) {
bz.bp.split(';').forEach(relayId => { bz.Relay.split(';').map(Number).filter(n => !isNaN(n) && n>=1 && n<=8).forEach(relayId => {
let id = parseInt(relayId, 10); cbRelay(relayId).prop('checked', true);
cbRelay(id).prop('checked', true);
}); });
} }
$broadcastzonemodal.off('click.broadcastzonesave').on('click.broadcastzonesave', '#broadcastzonesave', function () { $broadcastzonemodal.off('click.broadcastzonesave').on('click.broadcastzonesave', '#broadcastzonesave', function () {
let description = $broadcastzonedescription.val().trim(); let description = $broadcastzonedescription.val().trim();
@@ -262,10 +303,8 @@ $(document).ready(function () {
Relay: relay Relay: relay
}; };
fetchAPI(APIURL_BroadcastZone + "UpdateByIndex/" + bz.index, "PATCH", {}, bzUpdate, (okdata) => { fetchAPI(APIURL_BroadcastZone + "UpdateByIndex/" + bz.index, "PATCH", {}, bzUpdate, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone, () => { reloadBroadcastZones(APIURL_BroadcastZone);
fill_broadcastzonetablebody(window.BroadcastZoneList); alert("Success edit broadcast zone: " + okdata.message);
alert("Success edit broadcast zone: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error edit broadcast zone: " + errdata.message); alert("Error edit broadcast zone: " + errdata.message);
}); });
@@ -278,16 +317,14 @@ $(document).ready(function () {
} }
}); });
$btnExport.click(() => { $btnExport.off('click').on('click', () => {
DoExport(APIURL_BroadcastZone, "broadcastzones.xlsx", {}); DoExport(APIURL_BroadcastZone, "broadcastzones.xlsx", {});
}); });
$btnImport.click(() => { $btnImport.off('click').on('click', () => {
DoImport(APIURL_BroadcastZone, (okdata) => { DoImport(APIURL_BroadcastZone, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone, () => { reloadBroadcastZones(APIURL_BroadcastZone);
fill_broadcastzonetablebody(window.BroadcastZoneList); alert("Success import broadcast zones: " + okdata.message);
alert("Success import broadcast zones: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error importing broadcast zones from XLSX: " + errdata.message); alert("Error importing broadcast zones from XLSX: " + errdata.message);
}); });

View File

@@ -0,0 +1,7 @@
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bss-tooltip]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
})
}, false);

31
html/webpage/assets/js/dragdrop.js vendored Normal file
View File

@@ -0,0 +1,31 @@
const dropArea = document.getElementById("drop-area");
const fileInput = document.getElementById("file-input");
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, e => e.preventDefault());
dropArea.addEventListener(eventName, e => e.stopPropagation());
});
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'));
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'));
});
dropArea.addEventListener('click', () => fileInput.click());
dropArea.addEventListener('drop', e => {
const files = e.dataTransfer.files;
handleFiles(files);
});
fileInput.addEventListener('change', e => {
handleFiles(e.target.files);
});
function handleFiles(files) {
console.log("file dropped");
}

View File

@@ -1,4 +1,15 @@
/**
* @typedef {Object} LanguageBank
* @property {number} index
* @property {string} tag
* @property {string} language
*
*/
/** List of Languagebank data loaded from server
* @type {LanguageBank[]}
*/
window.languagebankdata = [];
/** /**
* Currently selected languagebank row in the table * Currently selected languagebank row in the table
* @type {JQuery<HTMLElement>|null} * @type {JQuery<HTMLElement>|null}
@@ -39,7 +50,22 @@ function fill_languagebanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length); $('#tablesize').text("Table Size: " + vv.length);
} }
/**
* Reload language bank from server
* @param {string} APIURL API URL endpoint, default "LanguageLink/"
*/
function reloadLanguageBank(APIURL = "LanguageLink/") {
window.languagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.languagebankdata.push(...okdata);
window.selectedlanguagerow = null;
fill_languagebanktablebody(window.languagebankdata);
}
}, (errdata) => {
alert("Error loading languagebank : " + errdata.message);
});
}
$(document).ready(function () { $(document).ready(function () {
console.log('languagebank.js loaded'); console.log('languagebank.js loaded');
@@ -89,15 +115,11 @@ $(document).ready(function () {
} }
}); });
reloadLanguageBank(APIURL, () => { reloadLanguageBank(APIURL);
fill_languagebanktablebody(window.languagebankdata);
});
$btnClear.click(() => { $btnClear.click(() => {
DoClear(APIURL, "LanguageLink", (okdata) => { DoClear(APIURL, "LanguageLink", (okdata) => {
reloadLanguageBank(APIURL, () => { reloadLanguageBank(APIURL);
fill_languagebanktablebody(window.languagebankdata); alert("Success clear languageLink : " + okdata.message);
alert("Success clear languageLink : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error clear languageLink : " + errdata.message); alert("Error clear languageLink : " + errdata.message);
}); });
@@ -134,10 +156,8 @@ $(document).ready(function () {
language: langString language: langString
} }
fetchAPI(APIURL + "Add", "POST", {}, ll, (okdata) => { fetchAPI(APIURL + "Add", "POST", {}, ll, (okdata) => {
reloadLanguageBank(APIURL, () => { alert("Success add language : " + okdata.message);
fill_languagebanktablebody(window.languagebankdata); reloadLanguageBank(APIURL);
alert("Success add language : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error add language : " + errdata.message); alert("Error add language : " + errdata.message);
}); });
@@ -155,16 +175,14 @@ $(document).ready(function () {
let cells = window.selectedlanguagerow.find('td'); let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */ /** @type {Language} */
let ll = { let ll = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
tag: cells.eq(1).text(), tag: cells.eq(1).text(),
language: cells.eq(2).text() language: cells.eq(2).text()
} }
if (confirm(`Are you sure to delete language [${ll.index}] Tag=${ll.tag} Language=${ll.language}?`)) { if (confirm(`Are you sure to delete language [${ll.index}] Tag=${ll.tag} Language=${ll.language}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + ll.index, "DELETE", {}, null, (okdata) => { fetchAPI(APIURL + "DeleteByIndex/" + ll.index, "DELETE", {}, null, (okdata) => {
reloadLanguageBank(APIURL, () => { reloadLanguageBank(APIURL);
fill_languagebanktablebody(window.languagebankdata); alert("Success delete language : " + okdata.message);
alert("Success delete language : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error delete language : " + errdata.message); alert("Error delete language : " + errdata.message);
}); });
@@ -176,7 +194,7 @@ $(document).ready(function () {
let cells = window.selectedlanguagerow.find('td'); let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */ /** @type {Language} */
let ll = { let ll = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
tag: cells.eq(1).text(), tag: cells.eq(1).text(),
language: cells.eq(2).text() language: cells.eq(2).text()
} }
@@ -221,10 +239,8 @@ $(document).ready(function () {
ll.tag = tag; ll.tag = tag;
ll.language = langString; ll.language = langString;
fetchAPI(APIURL + "UpdateByIndex/" + ll.index, "PATCH", {}, ll, (okdata) => { fetchAPI(APIURL + "UpdateByIndex/" + ll.index, "PATCH", {}, ll, (okdata) => {
reloadLanguageBank(APIURL, () => { reloadLanguageBank(APIURL);
fill_languagebanktablebody(window.languagebankdata); alert("Success edit language : " + okdata.message);
alert("Success edit language : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error edit language : " + errdata.message); alert("Error edit language : " + errdata.message);
}); });
@@ -246,10 +262,8 @@ $(document).ready(function () {
}); });
$btnImport.click(() => { $btnImport.click(() => {
DoImport(APIURL, (okdata) => { DoImport(APIURL, (okdata) => {
reloadLanguageBank(APIURL, () => { reloadLanguageBank(APIURL);
fill_languagebanktablebody(window.languagebankdata); alert("Success import languagebank : " + okdata.message);
alert("Success import languagebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error importing languagebank from XLSX : " + errdata.message); alert("Error importing languagebank from XLSX : " + errdata.message);
}); });

View File

@@ -1,4 +1,19 @@
/**
* @typedef {Object} MessageBank
* @property {number} index
* @property {string} description
* @property {string} language
* @property {number} aNN_ID
* @property {string} voice_Type
* @property {string} message_Detail
* @property {string} message_TAGS
*/
/**
* List of Messagebank data loaded from server
* @type {MessageBank[]}
*/
window.messagebankdata ??= [];
/** /**
* Currently selected messagebank row in the table * Currently selected messagebank row in the table
* @type {JQuery<HTMLElement>|null} * @type {JQuery<HTMLElement>|null}
@@ -44,7 +59,22 @@ function fill_messagebanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length); $('#tablesize').text("Table Size: " + vv.length);
} }
/**
* Reload message bank from server
* @param {string} APIURL API URL endpoint, default "MessageBank/"
*/
function reloadMessageBank(APIURL = "MessageBank/") {
window.messagebankdata ??= [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.messagebankdata.push(...okdata);
window.selectedmessagerow = null;
fill_messagebanktablebody(window.messagebankdata);
}
}, (errdata) => {
alert("Error loading messagebank : " + errdata.message);
});
}
$(document).ready(function () { $(document).ready(function () {
console.log("messagebank.js loaded"); console.log("messagebank.js loaded");
@@ -91,18 +121,33 @@ $(document).ready(function () {
function refill_messageavailablevariables() { function refill_messageavailablevariables() {
$messageavailablevariables.empty(); $messageavailablevariables.empty();
categories.forEach(cat => { categories.forEach(cat => {
$messageavailablevariables.append(ListItem(`{${cat}}`)); $messageavailablevariables.append(ListItem(`[${cat}]`));
}); });
if ($messagelanguage.val() && $messagevoicetype.val()) { let lang = $messagelanguage.val();
soundbankdata let vt = $messagevoicetype.val();
.filter(sb => sb.language.toLowerCase() === $messagelanguage.val().toLowerCase()) if (lang && lang.length > 0){
.filter(sb => sb.voiceType.toLowerCase() === $messagevoicetype.val().toLowerCase()) console.log("Selected Language:", lang);
.filter(sb => sb.category.toLowerCase() === "phrase") if (vt && vt.length > 0){
.forEach(sb => { console.log("Selected Voice Type:", vt);
$messageavailablevariables.append(ListItem(`[${sb.Description}]`));
fetchAPI(`SoundBank/GetPhrases/${lang}/${vt}`, "GET", {}, null, (okdata) => {
if (Array.isArray(okdata) && okdata.length > 0) {
console.log(`Loaded ${okdata.length} phrases from soundbank for language=${lang} and voiceType=${vt}`);
console.log(JSON.stringify(okdata));
okdata.forEach(sb => {
if (sb.description && sb.description.length > 0) {
$messageavailablevariables.append(ListItem(`${sb.description} [${sb.TAG}]`));
}
});
}
}, (errdata) => {
//alert("Error loading phrases from soundbank : " + errdata.message);
}); });
}
} }
} }
/** /**
@@ -113,7 +158,7 @@ $(document).ready(function () {
$messagedescription.val(''); $messagedescription.val('');
// fill messagelanguage options from languages[] // fill messagelanguage options from languages[]
$messagelanguage.empty(); $messagelanguage.empty();
languages.forEach(lang => { window.languages.forEach(lang => {
$messagelanguage.append(new Option(lang, lang)); $messagelanguage.append(new Option(lang, lang));
}); });
$messagelanguage.val(null); $messagelanguage.val(null);
@@ -124,7 +169,7 @@ $(document).ready(function () {
$messageannid.val(1); $messageannid.val(1);
// fill messagevoicetype options from voiceTypes[] // fill messagevoicetype options from voiceTypes[]
$messagevoicetype.empty(); $messagevoicetype.empty();
voiceTypes.forEach(vt => { window.voiceTypes.forEach(vt => {
$messagevoicetype.append(new Option(vt, vt)); $messagevoicetype.append(new Option(vt, vt));
}); });
$messagevoicetype.val(null); $messagevoicetype.val(null);
@@ -178,15 +223,11 @@ $(document).ready(function () {
}); });
reloadMessageBank(APIURL, () => { reloadMessageBank(APIURL);
fill_messagebanktablebody(window.messagebankdata);
});
$btnClear.click(() => { $btnClear.click(() => {
DoClear(APIURL, "Messagebank", (okdata) => { DoClear(APIURL, "Messagebank", (okdata) => {
reloadMessageBank(APIURL, () => { reloadMessageBank(APIURL);
fill_messagebanktablebody(window.messagebankdata); alert("Success clear messagebank : " + okdata.message);
alert("Success clear messagebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error clear messagebank : " + errdata.message); alert("Error clear messagebank : " + errdata.message);
}); });
@@ -261,10 +302,8 @@ $(document).ready(function () {
}; };
// send to server using fetchAPI // send to server using fetchAPI
fetchAPI(APIURL + "Add", "POST", mb, null, (okdata) => { fetchAPI(APIURL + "Add", "POST", mb, null, (okdata) => {
reloadMessageBank(APIURL, () => { reloadMessageBank(APIURL);
fill_messagebanktablebody(window.messagebankdata); alert("Success add new messagebank : " + okdata.message);
alert("Success add new messagebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error add new messagebank : " + errdata.message); alert("Error add new messagebank : " + errdata.message);
}); });
@@ -281,7 +320,7 @@ $(document).ready(function () {
let cells = window.selectedmessagerow.find('td'); let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */ /** @type {MessageBank} */
let mb = { let mb = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
language: cells.eq(2).text(), language: cells.eq(2).text(),
aNN_ID: parseInt(cells.eq(3).text()), aNN_ID: parseInt(cells.eq(3).text()),
@@ -292,10 +331,8 @@ $(document).ready(function () {
if (confirm(`Are you sure to delete messagebank [${mb.index}] Description=${mb.description}? ANN_ID=${mb.aNN_ID} Language=${mb.language} Voice_Type=${mb.voice_Type} `)) { if (confirm(`Are you sure to delete messagebank [${mb.index}] Description=${mb.description}? ANN_ID=${mb.aNN_ID} Language=${mb.language} Voice_Type=${mb.voice_Type} `)) {
fetchAPI(APIURL + "DeleteByIndex/" + mb.index, "DELETE", {}, null, (okdata) => { fetchAPI(APIURL + "DeleteByIndex/" + mb.index, "DELETE", {}, null, (okdata) => {
reloadMessageBank(APIURL, () => { reloadMessageBank(APIURL);
fill_messagebanktablebody(window.messagebankdata); alert("Success delete messagebank : " + okdata.message);
alert("Success delete messagebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error delete messagebank : " + errdata.message); alert("Error delete messagebank : " + errdata.message);
}); });
@@ -307,7 +344,7 @@ $(document).ready(function () {
let cells = window.selectedmessagerow.find('td'); let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */ /** @type {MessageBank} */
let mb = { let mb = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
language: cells.eq(2).text(), language: cells.eq(2).text(),
aNN_ID: parseInt(cells.eq(3).text()), aNN_ID: parseInt(cells.eq(3).text()),
@@ -322,19 +359,8 @@ $(document).ready(function () {
// Fill modal fields with selected messagebank data // Fill modal fields with selected messagebank data
$messageindex.val(mb.index).prop('disabled', true); $messageindex.val(mb.index).prop('disabled', true);
$messagedescription.val(mb.description); $messagedescription.val(mb.description);
// Fill messagelanguage options and select current
$messagelanguage.empty();
languages.forEach(lang => {
$messagelanguage.append(new Option(lang, lang));
});
$messagelanguage.val(mb.language); $messagelanguage.val(mb.language);
// Fill messagevoicetype options and select current
$messagevoicetype.empty();
voiceTypes.forEach(vt => {
$messagevoicetype.append(new Option(vt, vt));
});
$messagevoicetype.val(mb.voice_Type); $messagevoicetype.val(mb.voice_Type);
// Set annid
$messageannid.val(mb.aNN_ID); $messageannid.val(mb.aNN_ID);
// Refill message available variables // Refill message available variables
refill_messageavailablevariables(); refill_messageavailablevariables();
@@ -401,10 +427,8 @@ $(document).ready(function () {
Message_TAGS: messagetags Message_TAGS: messagetags
}; };
fetchAPI(APIURL + "UpdateByIndex/" + mb.index, "PATCH", mbUpdate, null, (okdata) => { fetchAPI(APIURL + "UpdateByIndex/" + mb.index, "PATCH", mbUpdate, null, (okdata) => {
reloadMessageBank(APIURL, () => { reloadMessageBank(APIURL);
fill_messagebanktablebody(window.messagebankdata); alert("Success edit messagebank : " + okdata.message);
alert("Success edit messagebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error edit messagebank : " + errdata.message); alert("Error edit messagebank : " + errdata.message);
}); });
@@ -423,11 +447,8 @@ $(document).ready(function () {
}); });
$btnImport.click(() => { $btnImport.click(() => {
DoImport(APIURL, (okdata) => { DoImport(APIURL, (okdata) => {
reloadMessageBank(APIURL, () => { reloadMessageBank(APIURL);
fill_messagebanktablebody(window.messagebankdata); alert("Success import messagebank : " + okdata.message);
alert("Success import messagebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error importing messagebank from XLSX : " + errdata.message); alert("Error importing messagebank from XLSX : " + errdata.message);
}); });

View File

@@ -0,0 +1,359 @@
/**
* @typedef {Object} StreamerOutputData
* @property {number} index - The index of the Barix connection.
* @property {string} channel - The channel name of the Barix connection.
* @property {string} ipaddress - The IP address of the Barix connection.
* @property {number} bufferRemain - The remaining buffer size of the Barix connection.
* @property {boolean} isPlaying - true = playback started, false = playback stopped
* @property {number} vu - The VU level of the Barix connection, 0 to 100.
*/
/**
* @typedef {Object} PagingQueue
* @property {number} index - The index of the paging queue item.
* @property {string} Date_Time - The date and time of the paging queue item.
* @property {string} Source - The source of the paging queue item.
* @property {string} Type - The type of the paging queue item.
* @property {string} Message - The message of the paging queue item.
* @property {string} BroadcastZones - The broadcast zones of the paging queue item.
*/
/**
* @typedef {Object} StreamerCard
* @property {JQuery<HTMLElement> | null} title - The jQuery result should be <h4> element.
* @property {JQuery<HTMLElement> | null} ip - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} buffer - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} status - The jQuery result should be <p> element.
* @property {JQuery<HTMLElement> | null} vu - The jQuery result should be <progress-bar> element.
*/
function getCardByIndex(index) {
let obj = {
// title is <h4> element wiht id `streamertitle${index}`, with index as two digit number, e.g. 01, 02, 03
title: $(`#streamertitle${index.toString().padStart(2, '0')}`),
// ip is <h6> element with id `streamerip${index}`, with index as two digit number, e.g. 01, 02, 03
ip: $(`#streamerip${index.toString().padStart(2, '0')}`),
// buffer is <h6> element with id `streamerbuffer${index}`, with index as two digit number, e.g. 01, 02, 03
buffer: $(`#streamerbuffer${index.toString().padStart(2, '0')}`),
// status is <p> element with id `streamerstatus${index}`, with index as two digit number, e.g. 01, 02, 03
status: $(`#streamerstatus${index.toString().padStart(2, '0')}`),
// vu is <progress-bar> element with id `streamervu${index}`, with index as two digit number, e.g. 01, 02, 03
vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`),
}
return obj;
}
/**
* Updates the streamer card with the provided values.
* @param {StreamerOutputData[]} values
*/
function UpdateStreamerCard(values) {
if (!Array.isArray(values) || values.length === 0) return;
function setProgress(index, $bar, value, max = 100) {
const v = Number(value ?? 0);
const pct = Math.max(0, Math.min(100, Math.round((v / max) * 100)));
//if (index!==1) return; // only update index 1 for testing
//console.log(`setProgress: index=${index}, value=${v}, pct=${pct}`);
$bar
.attr('aria-valuenow', v) // semantic value
.css('width', pct + '%') // visual width
.text(pct); // optional label
}
for (let i = 1; i <= 64; i++) {
let vv = values.find(v => v.index === i);
let card = getCardByIndex(i);
if (vv) {
// there is value for this index
if (card.title) card.title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`);
if (card.ip) card.ip.text(`IP Address: ${vv.ipaddress ? vv.ipaddress : 'N/A'}`);
if (card.buffer) card.buffer.text(`Buffer: ${vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A'}`);
if (card.status) card.status.text(`Status: ${vv.isPlaying ? 'Playing' : 'Stopped'}`);
if (card.vu) {
setProgress(i, card.vu, vv.vu, 100);
}
} else {
// no value for this index, disable the card
if (card.title) card.title.text(`Channel ${i.toString().padStart(2, '0')}`);
if (card.ip) card.ip.text(`IP Address: N/A`);
if (card.buffer) card.buffer.text(`Buffer: N/A`);
if (card.status) card.status.text(`Status: Disconnected`);
if (card.vu) {
setProgress(i, card.vu, 0, 100);
}
}
}
}
/**
* @type {PagingQueue[]}
*/
window.PagingQueue = [];
/**
* @type {JQuery<HTMLElement> | null}
*/
window.selectedpagingrow = null;
/**
* @typedef {Object} QueueTable
* @property {number} index - The index of the automatic queue item.
* @property {string} Date_Time - The date and time of the automatic queue item.
* @property {string} Source - The source of the automatic queue item.
* @property {string} Type - The type of the automatic queue item.
* @property {string} Message - The message of the automatic queue item.
* @property {string} SB_TAGS - The SB_TAGS of the automatic queue item.
* @property {string} BroadcastZones - The broadcast zones of the automatic queue item.
* @property {number} Repeat - The repeat count of the automatic queue item.
* @property {string} Language - The language of the automatic queue item.
*/
/**
* @type {QueueTable[]}
*/
window.QueueTable = [];
/**
* @type {JQuery<HTMLElement> | null}
*/
window.selectedautomaticrow = null;
/**
* Fills the paging queue table body with the provided data.
* @param {PagingQueue[]} vv array of PagingQueue objects
* @returns
*/
function fill_pagingqueuetablebody(vv) {
$('#pagingqueuetable').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
// fill index and description columns using item properties
$('#pagingqueuetable').append(`<tr>
<td>${item.index}</td>
<td>${item.date_Time}</td>
<td>${item.source}</td>
<td>${item.type}</td>
<td>${item.message}</td>
<td>${item.broadcastZones}</td>
</tr>`);
let $addedrow = $('#pagingqueuetable tr:last');
$addedrow.off('click').on('click', function () {
if (window.selectedpagingrow) {
window.selectedpagingrow.find('td').css('background-color', '');
if (window.selectedpagingrow.is($(this))) {
window.selectedpagingrow = null;
$('#removepagingqueue').prop('disabled', true);
return;
}
}
window.selectedpagingrow = $(this);
window.selectedpagingrow.find('td').css('background-color', 'lightblue');
$('#removepagingqueue').prop('disabled', false);
});
});
}
/**
* Fills the automatic queue table body with the provided data.
* @param {QueueTable[]} vv array of QueueTable objects
* @returns
*/
function fill_automaticqueuetablebody(vv) {
$('#automaticqueuetable').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
// fill index and description columns using item properties
//console.log("fill_automaticqueuetablebody: item", item);
$('#automaticqueuetable').append(`<tr>
<td>${item.index}</td>
<td>${item.date_Time}</td>
<td>${item.source}</td>
<td>${item.type}</td>
<td>${item.message}</td>
<td>${item.broadcastZones}</td>
</tr>`);
let $addedrow = $('#automaticqueuetable tr:last');
$addedrow.off('click').on('click', function () {
if (window.selectedautomaticrow) {
window.selectedautomaticrow.find('td').css('background-color', '');
if (window.selectedautomaticrow.is($(this))) {
window.selectedautomaticrow = null;
$('#removeautomatictable').prop('disabled', true);
return;
}
}
window.selectedautomaticrow = $(this);
window.selectedautomaticrow.find('td').css('background-color', 'lightblue');
$('#removeautomatictable').prop('disabled', false);
});
});
}
function reloadPagingQueue(APIURL = "QueuePaging/") {
window.PagingQueue = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata) && okdata.length > 0) {
window.PagingQueue.push(...okdata);
fill_pagingqueuetablebody(window.PagingQueue);
} else {
console.log("reloadPagingQueue: okdata is not array");
}
}, (errdata) => {
console.log("reloadPagingQueue: errdata", errdata);
});
}
function reloadAutomaticQueue(APIURL = "QueueTable/") {
window.QueueTable = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata) && okdata.length > 0) {
window.QueueTable.push(...okdata);
fill_automaticqueuetablebody(window.QueueTable);
} else {
console.log("reloadAutomaticQueue: okdata is not array");
}
}, (errdata) => {
console.log("reloadAutomaticQueue: errdata", errdata);
});
}
function RemovePagingQueueByIndex(index, APIURL = "QueuePaging/") {
fetchAPI(APIURL + "DeleteByIndex/" + index, "DELETE", {}, null, (okdata) => {
console.log("RemovePagingQueueByIndex: okdata", okdata);
reloadPagingQueue(APIURL);
}, (errdata) => {
console.log("RemovePagingQueueByIndex: errdata", errdata);
});
}
function RemoveAutomaticQueueByIndex(index, APIURL = "QueueTable/") {
fetchAPI(APIURL + "DeleteByIndex/" + index, "DELETE", {}, null, (okdata) => {
console.log("RemoveAutomaticQueueByIndex: okdata", okdata);
reloadAutomaticQueue(APIURL);
}, (errdata) => {
console.log("RemoveAutomaticQueueByIndex: errdata", errdata);
});
}
$(document).ready(function () {
console.log("overview.js loaded");
$('#clearpagingqueue').off('click').on('click', function () {
DoClear("QueuePaging/", "Paging Queue", (okdata) => {
reloadPagingQueue();
alert("Success clear Paging Queue: " + okdata.message);
}, (errdata) => {
alert("Error clear Paging Queue: " + errdata.message);
});
});
$('#removepagingqueue').off('click').on('click', function () {
if (window.selectedpagingrow) {
let cells = window.selectedpagingrow.find('td');
let index = Number(cells.eq(0).text());
let description = cells.eq(1).text();
if (!isNaN(index) && description && description.length > 0) {
if (confirm(`Are you sure to remove Paging Queue Index: ${index} Description: ${description} ?`)) {
RemovePagingQueueByIndex(index);
window.selectedpagingrow = null;
$('#removepagingqueue').prop('disabled', true);
}
}
}
});
$('#clearautomatictable').off('click').on('click', function () {
DoClear("QueueTable/", "Automatic Queue", (okdata) => {
reloadAutomaticQueue();
alert("Success clear Automatic Queue: " + okdata.message);
}, (errdata) => {
alert("Error clear Automatic Queue: " + errdata.message);
});
});
$('#removeautomatictable').off('click').on('click', function () {
if (window.selectedautomaticrow) {
let cells = window.selectedautomaticrow.find('td');
let index = Number(cells.eq(0).text());
let description = cells.eq(1).text();
if (!isNaN(index) && description && description.length > 0) {
if (confirm(`Are you sure to remove Automatic Queue Index: ${index} Description: ${description} ?`)) {
RemoveAutomaticQueueByIndex(index);
window.selectedautomaticrow = null;
$('#removeautomatictable').prop('disabled', true);
}
}
}
});
let intervaljob1 = null;
let intervaljob2 = null;
function runIntervalJob() {
if (intervaljob1) clearInterval(intervaljob1);
intervaljob1 = setInterval(() => {
sendCommand("getStreamerOutputs", "");
}, 100);
if (intervaljob2) clearInterval(intervaljob2);
intervaljob2 = setInterval(() => {
sendCommand("getPagingQueue", "");
sendCommand("getAASQueue", "");
}, 2000);
console.log("overview.js interval job started");
}
runIntervalJob();
window.addEventListener('ws_connected', () => {
console.log("overview.js ws_connected event triggered");
runIntervalJob();
});
window.addEventListener('ws_disconnected', () => {
console.log("overview.js ws_disconnected event triggered");
if (intervaljob) clearInterval(intervaljob);
intervaljob = null;
});
window.addEventListener('ws_message', (event) => {
let rep = event.detail;
let cmd = rep.reply;
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getPagingQueue":
let pq = JSON.parse(data);
//console.log("getPagingQueue:", pq);
window.PagingQueue = [];
if (Array.isArray(pq) && pq.length > 0) {
window.PagingQueue.push(...pq);
}
fill_pagingqueuetablebody(window.PagingQueue);
break;
case "getAASQueue":
let aq = JSON.parse(data);
//console.log("getAASQueue:", aq);
window.QueueTable = [];
if (Array.isArray(aq) && aq.length > 0) {
window.QueueTable.push(...aq);
}
fill_automaticqueuetablebody(window.QueueTable);
break;
case "getStreamerOutputs":
/**
* @type {StreamerOutputData[]}
*/
let so = JSON.parse(data);
UpdateStreamerCard(so);
break;
}
}
});
$(window).on('beforeunload', function () {
console.log("overview.js beforeunload event triggered");
clearInterval(intervaljob1);
clearInterval(intervaljob2);
intervaljob1 = null;
intervaljob2 = null;
});
});

View File

@@ -1,4 +1,20 @@
/**
* @typedef {Object} ScheduleBank
* @property {number} index
* @property {string} description
* @property {string} day
* @property {string} time
* @property {string} soundpath
* @property {number} repeat
* @property {boolean} enable
* @property {string} broadcastZones
* @property {string} language
*/
/** List of Schedulebank data loaded from server
* @type {ScheduleBank[]}
*/
window.schedulebankdata = [];
/** /**
* Currently selected schedulebank row in the table * Currently selected schedulebank row in the table
* @type {JQuery<HTMLElement>|null} * @type {JQuery<HTMLElement>|null}
@@ -45,7 +61,22 @@ function fill_schedulebanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length); $('#tablesize').text("Table Size: " + vv.length);
} }
/**
* Reload timer bank from server
* @param {string} APIURL API URL endpoint, default "ScheduleBank/"
*/
function reloadTimerBank(APIURL = "ScheduleBank/") {
window.schedulebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.schedulebankdata.push(...okdata);
selectedschedulerow = null;
fill_schedulebanktablebody(window.schedulebankdata);
}
}, (errdata) => {
alert("Error loading schedulebank : " + errdata.message);
});
}
$(document).ready(function () { $(document).ready(function () {
console.log("schedulebank.js loaded successfully"); console.log("schedulebank.js loaded successfully");
@@ -70,28 +101,30 @@ $(document).ready(function () {
let $schedulehour = $schedulemodal.find('#schedulehour'); let $schedulehour = $schedulemodal.find('#schedulehour');
// number input 0-59 // number input 0-59
let $scheduleminute = $schedulemodal.find('#scheduleminute'); let $scheduleminute = $schedulemodal.find('#scheduleminute');
// select for messagebank // select2 for message
let $schedulesoundpath = $schedulemodal.find('#schedulesoundpath'); let $schedulemessage = $schedulemodal.find('#schedulemessage');
$schedulemessage.select2({});
// number input 0-5 // number input 0-5
let $schedulerepeat = $schedulemodal.find('#schedulerepeat'); let $schedulerepeat = $schedulemodal.find('#schedulerepeat');
// checkbox // checkbox
let $scheduleenable = $schedulemodal.find('#scheduleenable'); let $scheduleenable = $schedulemodal.find('#scheduleenable');
// div for list of checkboxes // select2 for broadcastzones
let $schedulezones = $schedulemodal.find('#schedulezones'); let $schedulezones = $schedulemodal.find('#schedulezones');
$schedulezones.select2({});
// radio button for everyday // radio button for everyday
let $scheduleeveryday = $schedulemodal.find('#scheduleeveryday'); let $scheduleeveryday = $schedulemodal.find('#scheduleeveryday');
// radio button for weekdays // radio button for weekly
let $schedulesunday = $schedulemodal.find('#schedulesunday'); let $scheduleweekly = $schedulemodal.find('#scheduleweekly');
let $schedulemonday = $schedulemodal.find('#schedulemonday'); // select2 for weekly selection
let $scheduletuesday = $schedulemodal.find('#scheduletuesday'); let $weeklyselect = $schedulemodal.find('#weeklyselect');
let $schedulewednesday = $schedulemodal.find('#schedulewednesday'); $weeklyselect.select2({});
let $schedulethursday = $schedulemodal.find('#schedulethursday');
let $schedulefriday = $schedulemodal.find('#schedulefriday');
let $schedulesaturday = $schedulemodal.find('#schedulesaturday');
// radio button for specific date // radio button for specific date
let $schedulespecialdate = $schedulemodal.find('#schedulespecialdate'); let $schedulespecialdate = $schedulemodal.find('#schedulespecialdate');
// date input // date input
let $scheduledate = $schedulemodal.find('#scheduledate'); let $scheduledate = $schedulemodal.find('#scheduledate');
// select2 for language
let $languageselect = $schedulemodal.find('#languageselect');
$languageselect.select2({});
$schedulespecialdate.on('change', function () { $schedulespecialdate.on('change', function () {
if ($(this).is(':checked')) { if ($(this).is(':checked')) {
@@ -106,26 +139,10 @@ $(document).ready(function () {
$scheduledescription.val(''); $scheduledescription.val('');
$schedulehour.val('0'); $schedulehour.val('0');
$scheduleminute.val('0'); $scheduleminute.val('0');
$schedulerepeat.val('0');
$schedulesoundpath.empty();
if (Array.isArray(window.messagebankdata) && window.messagebankdata.length > 0) {
window.messagebankdata.forEach(item => {
let str = item.description+" ["+item.aNN_ID+"]";
let option = `<option value="${str}">${str}</option>`;
if ($schedulesoundpath.find(`option[value="${str}"]`).length === 0) $schedulesoundpath.append(option); // check if $schedulesoundpath already has this option
})
}
$schedulerepeat.val('1');
$scheduleenable.prop('checked', true); $scheduleenable.prop('checked', true);
$scheduleeveryday.prop('checked', false); $scheduleeveryday.prop('checked', false);
$schedulesunday.prop('checked', false);
$schedulemonday.prop('checked', false);
$scheduletuesday.prop('checked', false);
$schedulewednesday.prop('checked', false);
$schedulethursday.prop('checked', false);
$schedulefriday.prop('checked', false);
$schedulesaturday.prop('checked', false);
$schedulespecialdate.prop('checked', false); $schedulespecialdate.prop('checked', false);
$scheduledate.prop('disabled', true).val(''); $scheduledate.prop('disabled', true).val('');
@@ -149,15 +166,11 @@ $(document).ready(function () {
}); });
reloadTimerBank(APIURL, () => { reloadTimerBank(APIURL);
fill_schedulebanktablebody(window.schedulebankdata);
});
$btnClear.click(() => { $btnClear.click(() => {
DoClear(APIURL, "Timerbank", (okdata) => { DoClear(APIURL, "Timerbank", (okdata) => {
reloadTimerBank(APIURL,() => { reloadTimerBank(APIURL);
fill_schedulebanktablebody(window.schedulebankdata); alert("Success clear schedulebank : " + okdata.message);
alert("Success clear schedulebank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error clear schedulebank : " + errdata.message); alert("Error clear schedulebank : " + errdata.message);
}); });
@@ -218,10 +231,8 @@ $(document).ready(function () {
}; };
fetchAPI(APIURL + "Add", "POST", {}, scheduleObj, (okdata) => { fetchAPI(APIURL + "Add", "POST", {}, scheduleObj, (okdata) => {
reloadTimerBank(APIURL, () => { alert("Success add schedule: " + okdata.message);
fill_schedulebanktablebody(window.schedulebankdata); reloadTimerBank(APIURL);
alert("Success add schedule: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error add schedule: " + errdata.message); alert("Error add schedule: " + errdata.message);
}); });
@@ -234,7 +245,7 @@ $(document).ready(function () {
let cells = window.selectedschedulerow.find('td'); let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */ /** @type {ScheduleBank} */
let sr = { let sr = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
day: cells.eq(2).text(), day: cells.eq(2).text(),
time: cells.eq(3).text(), time: cells.eq(3).text(),
@@ -246,10 +257,8 @@ $(document).ready(function () {
} }
if (confirm(`Are you sure to delete schedule [${sr.index}] Description=${sr.description}?`)) { if (confirm(`Are you sure to delete schedule [${sr.index}] Description=${sr.description}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sr.index, "DELETE", {}, null, (okdata) => { fetchAPI(APIURL + "DeleteByIndex/" + sr.index, "DELETE", {}, null, (okdata) => {
reloadTimerBank(APIURL, () => { reloadTimerBank(APIURL);
fill_schedulebanktablebody(window.schedulebankdata); alert("Success delete schedule : " + okdata.message);
alert("Success delete schedule : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error delete schedule : " + errdata.message); alert("Error delete schedule : " + errdata.message);
}); });
@@ -261,7 +270,7 @@ $(document).ready(function () {
let cells = window.selectedschedulerow.find('td'); let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */ /** @type {ScheduleBank} */
let sr = { let sr = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
day: cells.eq(2).text(), day: cells.eq(2).text(),
time: cells.eq(3).text(), time: cells.eq(3).text(),
@@ -366,10 +375,8 @@ $(document).ready(function () {
}; };
fetchAPI(APIURL + "UpdateByIndex/" + sr.index, "PATCH", {}, scheduleObj, (okdata) => { fetchAPI(APIURL + "UpdateByIndex/" + sr.index, "PATCH", {}, scheduleObj, (okdata) => {
reloadTimerBank(APIURL, () => { alert("Success edit schedule: " + okdata.message);
fill_schedulebanktablebody(window.schedulebankdata); reloadTimerBank(APIURL);
alert("Success edit schedule: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error edit schedule: " + errdata.message); alert("Error edit schedule: " + errdata.message);
}); });
@@ -384,10 +391,8 @@ $(document).ready(function () {
}); });
$btnImport.click(() => { $btnImport.click(() => {
DoImport(APIURL, (okdata) => { DoImport(APIURL, (okdata) => {
reloadTimerBank(APIURL, () => { reloadTimerBank(APIURL);
fill_schedulebanktablebody(window.schedulebankdata); alert("Success import schedulebank from XLSX : " + okdata.message);
alert("Success import schedulebank from XLSX : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error importing schedulebank from XLSX : " + errdata.message); alert("Error importing schedulebank from XLSX : " + errdata.message);
}); });

View File

@@ -20,263 +20,6 @@ window.languages = [];
*/ */
window.scheduledays = [] window.scheduledays = []
/**
* @typedef {Object} MessageBank
* @property {number} index
* @property {string} description
* @property {string} language
* @property {number} aNN_ID
* @property {string} voice_Type
* @property {string} message_Detail
* @property {string} message_TAGS
*/
/**
* List of Messagebank data loaded from server
* taruh di sini karena dipakai di banyak tempat
* @type {MessageBank[]}
*/
window.messagebankdata ??= [];
/**
* Reload message bank from server
* @param {string} APIURL API URL endpoint, default "MessageBank/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadMessageBank(APIURL = "MessageBank/", callback=null) {
window.messagebankdata ??= [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.messagebankdata.push(...okdata);
console.log("Loaded " + window.messagebankdata.length + " message bank items");
window.selectedmessagerow = null;
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading messagebank : " + errdata.message);
});
}
/**
* @typedef {Object} ScheduleBank
* @property {number} index
* @property {string} description
* @property {string} day
* @property {string} time
* @property {string} soundpath
* @property {number} repeat
* @property {boolean} enable
* @property {string} broadcastZones
* @property {string} language
*/
/** List of Schedulebank data loaded from server
* @type {ScheduleBank[]}
*/
window.schedulebankdata = [];
/**
* Reload timer bank from server
* taruh di sini karena dipakai di banyak tempat
* @param {string} APIURL API URL endpoint, default "ScheduleBank/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadTimerBank(APIURL = "ScheduleBank/", callback=null) {
window.schedulebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.schedulebankdata.push(...okdata);
selectedschedulerow = null;
console.log("Loaded " + window.schedulebankdata.length + " schedule bank items");
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading schedulebank : " + errdata.message);
});
}
/**
* @typedef {Object} BroadcastZone
* @property {number} index
* @property {string} description
* @property {String} SoundChannel
* @property {String} Box
* @property {String} Relay
*/
/**
* List of broadcast zones available
* @type {BroadcastZone[]}
*/
window.BroadcastZoneList ??= [];
/**
* Reload broadcast zones from server
* taruh di sini karena dipakai di banyak tempat
* @param {String} APIURL API URL endpoint (default "BroadcastZones/")
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadBroadcastZones(APIURL = "BroadcastZones/", callback=null) {
window.BroadcastZoneList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadBroadcastZones : ", okdata)
window.BroadcastZoneList.push(...okdata);
console.log("Loaded " + window.BroadcastZoneList.length + " broadcast zone items");
window.selectedbroadcastzonerow = null;
if (callback && typeof callback === 'function') callback();
} else console.log("reloadBroadcastZones: okdata is not array");
}, (errdata) => {
alert("Error loading broadcast zones : " + errdata.message);
});
}
/**
* @typedef {Object} SoundBank
* @property {number} index
* @property {string} description
* @property {string} tag
* @property {string} category
* @property {string} language
* @property {string} voiceType
* @property {string} path
*/
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* List of Soundbank data loaded from server
* taruh di sini karena dipakai di banyak tempat
* @type {SoundBank[]}
*/
window.soundbankdata = [];
/**
* List of sound files in the soundbank directory, that ends with .wav or .mp3
* taruh di sini karena dipakai di banyak tempat
* @type {string[]}
*/
window.soundbankfiles = [];
/**
* Select2 data source
* See https://select2.org/data-sources/formats
* @type {Select2item[]}
*/
window.select2data = [];
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadSoundBank(APIURL = "SoundBank/", callback=null) {
window.soundbankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.soundbankdata.push(...okdata);
window.selectedsoundrow = null;
console.log("Loaded " + window.soundbankdata.length + " sound bank items");
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading soundbank : " + errdata.message);
});
}
/**
* Reload soundbank files from server
* @param {String} APIURL API URL endpoint (default "SoundBank/")
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadSoundbankFiles(APIURL = "SoundBank/",callback=null) {
window.soundbankfiles = [];
fetchAPI(APIURL + "ListFiles", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
window.soundbankfiles = okdata.filter(item => item.trim().length > 0);
// refill select2data
window.select2data = window.soundbankfiles.map((item, index) => ({ id: index + 1, text: item }));
console.log("Loaded " + window.soundbankfiles.length + " sound bank files");
if (callback && typeof callback === 'function') callback();
} else console.log("reloadSoundbankFiles: okdata is not array");
}, (errdata) => {
alert("Error loading soundbank files : " + errdata.message);
});
}
/**
* @typedef {Object} SoundChannel
* @property {number} index - The index of the sound channel.
* @property {string} channel - The name of the sound channel.
* @property {string} ip - The IP address associated with the sound channel.
*/
/**
* @type {SoundChannel[]}
*/
window.soundChannels = [];
/**
* Reload sound channels from server
* @param {String} APIURL API URL endpoint (default "SoundChannel/")
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadSoundChannel(APIURL = "SoundChannel/", callback=null) {
window.soundChannels = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadSoundChannel : ", okdata)
window.soundChannels.push(...okdata);
window.selectedsoundchannelrow = null;
console.log("Loaded " + window.soundChannels.length + " sound channel items");
if (callback && typeof callback === 'function') callback();
} else console.log("reloadSoundChannel: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
/**
* @typedef {Object} LanguageBank
* @property {number} index
* @property {string} tag
* @property {string} language
*
*/
/** List of Languagebank data loaded from server
* @type {LanguageBank[]}
*/
window.languagebankdata = [];
/**
* Reload language bank from server
* @param {string} APIURL API URL endpoint, default "LanguageLink/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadLanguageBank(APIURL = "LanguageLink/", callback=null) {
window.languagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.languagebankdata.push(...okdata);
window.selectedlanguagerow = null;
console.log("Loaded " + window.languagebankdata.length + " language bank items");
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading languagebank : " + errdata.message);
});
}
/** /**
* Create a list item element * Create a list item element
* @param {String} text Text Content for the list item * @param {String} text Text Content for the list item
@@ -326,10 +69,10 @@ function fetchAPI(endpoint, method, headers = {}, body = null, cbOK, cbError) {
} }
} }
fetch(url, options) fetch(url, options)
.then(async(response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
let msg ; let msg;
try{ try {
let _xxx = await response.json(); let _xxx = await response.json();
msg = _xxx.message || response.statusText; msg = _xxx.message || response.statusText;
} catch { } catch {
@@ -538,10 +281,10 @@ window.redcircle = null;
*/ */
$(document).ready(function () { $(document).ready(function () {
document.title = "Automatic Announcement System" document.title = "Automatic Announcement System"
if (window.greencircle === null){ if (window.greencircle === null) {
fetchImg('green_circle.png', (url) => { window.greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); }); fetchImg('green_circle.png', (url) => { window.greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); });
} }
if (window.redcircle === null){ if (window.redcircle === null) {
fetchImg('red_circle.png', (url) => { window.redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); }); fetchImg('red_circle.png', (url) => { window.redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); });
} }
const wsURL = window.location.pathname + '/ws' const wsURL = window.location.pathname + '/ws'
@@ -568,60 +311,83 @@ $(document).ready(function () {
getCategories(); getCategories();
getLanguages(); getLanguages();
getScheduledDays(); getScheduledDays();
reloadMessageBank();
reloadTimerBank();
reloadBroadcastZones();
reloadSoundBank();
reloadSoundbankFiles();
reloadSoundChannel();
reloadLanguageBank();
// Initialize WebSocket connection
window.ws = new WebSocket(wsURL);
window.ws.onopen = () => { // reconnect handle
console.log('WebSocket connection established'); let ws_reconnect;
$('#onlineindicator').attr('src', window.greencircle);
}; function reconnect() {
window.ws.onmessage = (event) => { if (window.ws && window.ws.readyState === WebSocket.OPEN) return;
if ($('#onlineindicator').attr('src') !== window.greencircle) { const s = new WebSocket(wsURL);
s.addEventListener('open', () => {
console.log('WebSocket connection established');
$('#onlineindicator').attr('src', window.greencircle); $('#onlineindicator').attr('src', window.greencircle);
}
let rep = JSON.parse(event.data);
let cmd = rep.reply
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getCPUStatus":
$('#cpustatus').text("CPU : " + data)
break;
case "getMemoryStatus":
$('#ramstatus').text("RAM : " + data)
break;
case "getDiskStatus":
$('#diskstatus').text("Disk : " + data)
break;
case "getNetworkStatus":
$('#networkstatus').text("Network : " + data)
break;
case "getSystemTime":
$('#datetimetext').text(data)
break;
if (ws_reconnect) {
// stop reconnect attempts
clearTimeout(ws_reconnect);
ws_reconnect = null;
} }
window.dispatchEvent(new Event('ws_connected'));
});
s.addEventListener('close', () => {
console.log('WebSocket connection closed');
window.dispatchEvent(new Event('ws_disconnected'));
resetStatusIndicators();
if (!ws_reconnect) {
clearTimeout(ws_reconnect);
ws_reconnect = null;
}
ws_reconnect = setTimeout(reconnect, 5000); // try to reconnect every 5 seconds
});
s.addEventListener('message', (event) => {
if ($('#onlineindicator').attr('src') !== window.greencircle) {
$('#onlineindicator').attr('src', window.greencircle);
}
let rep = JSON.parse(event.data);
window.dispatchEvent(new CustomEvent('ws_message', { detail: rep }));
let cmd = rep.reply
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getCPUStatus":
$('#cpustatus').text("CPU : " + data)
break;
case "getMemoryStatus":
$('#ramstatus').text("RAM : " + data)
break;
case "getDiskStatus":
$('#diskstatus').text("Disk : " + data)
break;
case "getNetworkStatus":
let result = "";
let json = JSON.parse(data);
if (Array.isArray(json) && json.length > 0) {
json.forEach((net) => {
if (result.length > 0) result += "\n"
result += `${net.displayName} (${net.ipV4addr.join(";")}) TX:${(net.txSpeed / 1024).toFixed(1)} KB/s RX:${(net.rxSpeed / 1024).toFixed(1)} KB/s`
})
} else result = "N/A";
$('#networkstatus').text(result)
break;
case "getSystemTime":
$('#datetimetext').text(data)
break;
}
}
});
window.ws = s;
}
reconnect();
window.addEventListener('beforeunload', () => {
try{
window.ws?.close(1000, "Client closed connection");
} catch (error) {
console.error("Error closing WebSocket connection:", error);
} }
}; });
window.ws.onclose = () => {
console.log('WebSocket connection closed');
resetStatusIndicators();
};
// window.ws.onerror = (error) => {
// console.error('WebSocket error:', error);
// };
setInterval(() => { setInterval(() => {
sendCommand("getCPUStatus", "") sendCommand("getCPUStatus", "")

2
html/webpage/assets/js/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,27 @@
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* @typedef {Object} SoundBank
* @property {number} index
* @property {string} description
* @property {string} tag
* @property {string} category
* @property {string} language
* @property {string} voiceType
* @property {string} path
*/
/**
* List of Soundbank data loaded from server
* @type {SoundBank[]}
*/
window.soundbankdata = [];
/** /**
* Currently selected soundbank row in the table * Currently selected soundbank row in the table
* @type {JQuery<HTMLElement>|null} * @type {JQuery<HTMLElement>|null}
@@ -7,13 +30,37 @@ window.selectedsoundrow = null;
/**
* Select2 data source
* See https://select2.org/data-sources/formats
* @type {Select2item[]}
*/
window.select2data = [];
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
*/
function reloadSoundBank(APIURL = "SoundBank/") {
window.soundbankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.soundbankdata.push(...okdata);
window.selectedsoundrow = null;
fill_soundbanktablebody(window.soundbankdata);
}
}, (errdata) => {
alert("Error loading soundbank : " + errdata.message);
});
}
/** /**
* Fill soundbank table body with values * Fill soundbank table body with values
* @param {SoundBank[]} vv values to fill * @param {SoundBank[]} vv values to fill
*/ */
function fill_soundbanktablebody(vv) { function fill_soundbanktablebody(vv) {
$('#soundbanktablebody').empty(); $('#soundbanktablebody').empty();
console.log("Filling soundbank table with " + vv.length + " items");
if (!Array.isArray(vv) || vv.length === 0) return; if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => { vv.forEach(item => {
const row = `<tr> const row = `<tr>
@@ -48,12 +95,60 @@ function fill_soundbanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length); $('#tablesize').text("Table Size: " + vv.length);
} }
/**
* Reload soundbank files from server and filter by language, category, and voiceType
* @param {String} language
* @param {String} category
* @param {String} voiceType
* @param {Function} cb callback function when done
*/
function reloadSoundbankFiles(language, category, voiceType, cb=null) {
window.select2data = [];
$('#modalpath').empty().trigger('change');
if (language && language.length > 0) {
if (category && category.length > 0) {
if (voiceType && voiceType.length > 0) {
fetchAPI(`ListFiles/${language}/${voiceType}/${category}`, "GET", {}, null, (okdata) => {
console.log("reloadSoundbankFiles: got " + okdata.length + " items");
if (Array.isArray(okdata)){
window.select2data = okdata.map(p => ({id: getFilenameFromPath(p), text: getFilenameFromPath(p)}));
$('#modalpath').select2({
data: window.select2data,
placeholder: 'Select a sound file',
allowClear: true,
width: '100%',
dropdownParent: $('#soundbankmodal')
});
if (cb) cb();
}
}, (errdata) => {
alert("Error loading soundbank files : " + errdata.message);
});
}
}
}
}
function getFilenameFromPath(path) {
if (!path || path.length === 0) return "";
if (path.includes('\\')) {
let parts = path.split('\\');
return parts[parts.length - 1];
} else if (!path.includes('/')) {
let parts = path.split('/');
return parts[parts.length - 1];
}
}
$(document).ready(function () { $(document).ready(function () {
console.log("soundbank.js loaded successfully"); console.log("soundbank.js loaded successfully");
reloadSoundbankFiles();
$('#soundbanktablebody').empty(); $('#soundbanktablebody').empty();
window.selectedsoundrow = null; window.selectedsoundrow = null;
let $btnClear = $('#btnClear'); let $btnClear = $('#btnClear');
@@ -72,7 +167,9 @@ $(document).ready(function () {
let $modalcategory = $modal.find('#modalcategory'); let $modalcategory = $modal.find('#modalcategory');
let $modallanguage = $modal.find('#modallanguage'); let $modallanguage = $modal.find('#modallanguage');
let $modalvoicetype = $modal.find('#modalvoicetype'); let $modalvoicetype = $modal.find('#modalvoicetype');
let $modalpath = $modal.find('#modalpath'); let selected_category = null;
let selected_language = null;
let selected_voicetype = null;
/** /**
* Clear soundbank modal inputs * Clear soundbank modal inputs
@@ -99,18 +196,11 @@ $(document).ready(function () {
$modalvoicetype.append(new Option(vt, vt)); $modalvoicetype.append(new Option(vt, vt));
}); });
$modalvoicetype.val(null); $modalvoicetype.val(null);
// fill modalpath options from soundbankfiles[] $('#modalpath').select2()
// TODO read https://jeesite.com/front/jquery-select2/4.0/index.htm
console.log("window.select2data has " + window.select2data.length + " items");
$('#modalpath').select2({
data: window.select2data
})
} }
reloadSoundBank(APIURL, () => { reloadSoundBank(APIURL);
fill_soundbanktablebody(window.soundbankdata);
});
$('#findsoundbank').on('input', function () { $('#findsoundbank').on('input', function () {
let searchTerm = $(this).val().trim().toLowerCase(); let searchTerm = $(this).val().trim().toLowerCase();
if (searchTerm.length > 0) { if (searchTerm.length > 0) {
@@ -124,23 +214,88 @@ $(document).ready(function () {
}); });
$btnClear.click(() => { $btnClear.click(() => {
DoClear(APIURL, "Soundbank", (okdata) => { DoClear(APIURL, "Soundbank", (okdata) => {
reloadSoundBank(APIURL, () => { reloadSoundBank(APIURL);
fill_soundbanktablebody(window.soundbankdata); alert("Success clear soundbank : " + okdata.message);
alert("Success clear soundbank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error clear soundbank : " + errdata.message); alert("Error clear soundbank : " + errdata.message);
}); });
}); });
function SetupEventForCategoryLanguageVoiceType() {
$modalcategory.off('change').on('change', function () {
selected_category = $(this).val();
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype);
});
$modallanguage.off('change').on('change', function () {
selected_language = $(this).val();
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype);
});
$modalvoicetype.off('change').on('change', function () {
selected_voicetype = $(this).val();
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype);
});
}
$btnAdd.click(() => { $btnAdd.click(() => {
$modal.modal('show'); $modal.modal('show');
clearSoundbankModal(); clearSoundbankModal();
// event on selection change of language, category, voiceType
SetupEventForCategoryLanguageVoiceType();
// event on Click save button // event on Click save button
$modal.off('click.soundbanksave').on('click.soundbanksave', '#soundbanksave', function () { $modal.off('click.soundbanksave').on('click.soundbanksave', '#soundbanksave', function () {
//TODO Add soundbank save process here let description = $modaldescription.val().trim();
let tag = $modaltag.val().trim();
let category = $modalcategory.val();
let language = $modallanguage.val();
let voiceType = $modalvoicetype.val();
let path = $('#soundbankmodal #modalpath').val();
if (!description || description.length === 0) {
alert("Description is required");
return;
}
if (!tag || tag.length === 0) {
alert("Tag is required");
return;
}
if (!category || category.length === 0) {
alert("Category is required");
return;
}
if (!language || language.length === 0) {
alert("Language is required");
return;
}
if (!voiceType || voiceType.length === 0) {
alert("Voice Type is required");
return;
}
if (!path || path.length === 0) {
alert("Path is required");
return;
}
$modal.modal('hide'); $modal.modal('hide');
/**
* @type {SoundBank}
*/
let nsb = {
index: 0,
Description: description,
TAG: tag,
Category: category,
Language: language,
VoiceType: voiceType,
Path: path
}
fetchAPI(APIURL + "Add", "POST", {}, nsb, (okdata) => {
reloadSoundBank(APIURL);
alert("Success add soundbank : " + okdata.message);
}, (errdata) => {
alert("Error add soundbank : " + errdata.message);
});
}); });
// event on Click close button // event on Click close button
$modal.off('click.soundbankclose').on('click.soundbankclose', '#soundbankclose', function () { $modal.off('click.soundbankclose').on('click.soundbankclose', '#soundbankclose', function () {
@@ -152,7 +307,7 @@ $(document).ready(function () {
let cells = window.selectedsoundrow.find('td'); let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */ /** @type {SoundBank} */
let sb = { let sb = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), description: cells.eq(1).text(),
tag: cells.eq(2).text(), tag: cells.eq(2).text(),
category: cells.eq(3).text(), category: cells.eq(3).text(),
@@ -162,10 +317,8 @@ $(document).ready(function () {
} }
if (confirm(`Are you sure to delete soundbank [${sb.index}] Description=${sb.description} Tag=${sb.tag}?`)) { if (confirm(`Are you sure to delete soundbank [${sb.index}] Description=${sb.description} Tag=${sb.tag}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sb.index, "DELETE", {}, null, (okdata) => { fetchAPI(APIURL + "DeleteByIndex/" + sb.index, "DELETE", {}, null, (okdata) => {
reloadSoundBank(APIURL, () => { reloadSoundBank(APIURL);
fill_soundbanktablebody(window.soundbankdata); alert("Success delete soundbank : " + okdata.message);
alert("Success delete soundbank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error delete soundbank : " + errdata.message); alert("Error delete soundbank : " + errdata.message);
}); });
@@ -177,28 +330,91 @@ $(document).ready(function () {
let cells = window.selectedsoundrow.find('td'); let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */ /** @type {SoundBank} */
let sb = { let sb = {
index: cells.eq(0).text(), index: Number(cells.eq(0).text()),
description: cells.eq(1).text(), Description: cells.eq(1).text(),
tag: cells.eq(2).text(), TAG: cells.eq(2).text(),
category: cells.eq(3).text(), Category: cells.eq(3).text(),
language: cells.eq(4).text(), Language: cells.eq(4).text(),
voiceType: cells.eq(5).text(), VoiceType: cells.eq(5).text(),
path: cells.eq(6).text() Path: cells.eq(6).text()
} }
if (confirm(`Are you sure to edit soundbank [${sb.index}] Description=${sb.description} Tag=${sb.tag}?`)) { if (confirm(`Are you sure to edit soundbank [${sb.index}] Description=${sb.Description} Tag=${sb.TAG}?`)) {
$modal.modal('show'); $modal.modal('show');
$modal.off('hidden.bs.modal').on('hidden.bs.modal', function () { clearSoundbankModal();
SetupEventForCategoryLanguageVoiceType();
$modalindex.val(sb.index).prop('disabled', true);
$modaldescription.val(sb.Description);
$modaltag.val(sb.TAG);
$modalcategory.val(sb.Category);
selected_category = sb.Category;
$modallanguage.val(sb.Language);
selected_language = sb.Language;
$modalvoicetype.val(sb.VoiceType);
selected_voicetype = sb.VoiceType;
// load soundbank files for selected language, category, voiceType
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype, () => {
// set selected path
$('#modalpath').val(getFilenameFromPath(sb.Path)).trigger('change');
});
// event on Click save button
// event on Click save button
$modal.off('click.soundbanksave').on('click.soundbanksave', '#soundbanksave', function () { $modal.off('click.soundbanksave').on('click.soundbanksave', '#soundbanksave', function () {
//TODO Add soundbank save process here let description = $modaldescription.val().trim();
let tag = $modaltag.val().trim();
let category = $modalcategory.val();
let language = $modallanguage.val();
let voiceType = $modalvoicetype.val();
let path = $('#soundbankmodal #modalpath').val();
if (!description || description.length === 0) {
alert("Description is required");
return;
}
if (!tag || tag.length === 0) {
alert("Tag is required");
return;
}
if (!category || category.length === 0) {
alert("Category is required");
return;
}
if (!language || language.length === 0) {
alert("Language is required");
return;
}
if (!voiceType || voiceType.length === 0) {
alert("Voice Type is required");
return;
}
if (!path || path.length === 0) {
alert("Path is required");
return;
}
if (description === sb.Description && tag === sb.TAG && category === sb.Category && language === sb.Language && voiceType === sb.VoiceType && path === sb.Path) {
alert("No changes detected");
return;
}
sb.Description = description;
sb.TAG = tag;
sb.Category = category;
sb.Language = language;
sb.VoiceType = voiceType;
sb.Path = path;
fetchAPI(APIURL + "UpdateByIndex/" + sb.index, "PATCH", {}, sb, (okdata) => {
reloadSoundBank(APIURL);
alert("Success update soundbank : " + okdata.message);
}, (errdata) => {
alert("Error update soundbank : " + errdata.message);
});
$modal.modal('hide'); $modal.modal('hide');
}); });
// event on Click close button // event on Click close button
$modal.off('click.soundbankclose').on('click.soundbankclose', '#soundbankclose', function () { $modal.off('click.soundbankclose').on('click.soundbankclose', '#soundbankclose', function () {
$modal.modal('hide'); $modal.modal('hide');
}); });
});
} }
} }
}); });
@@ -207,10 +423,8 @@ $(document).ready(function () {
}); });
$btnImport.click(() => { $btnImport.click(() => {
DoImport(APIURL, (okdata) => { DoImport(APIURL, (okdata) => {
reloadSoundBank(APIURL, () => { reloadSoundBank(APIURL);
fill_soundbanktablebody(window.soundbankdata); alert("Success import soundbank : " + okdata.message);
alert("Success import soundbank : " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error importing soundbank from XLSX : " + errdata.message); alert("Error importing soundbank from XLSX : " + errdata.message);
}); });

View File

@@ -1,4 +1,14 @@
/**
* @typedef {Object} SoundChannel
* @property {number} index - The index of the sound channel.
* @property {string} channel - The name of the sound channel.
* @property {string} ip - The IP address associated with the sound channel.
*/
/**
* @type {SoundChannel[]}
*/
window.soundChannels = [];
// Currently selected sound channel row in the table // Currently selected sound channel row in the table
window.selectedSoundChannel = null; window.selectedSoundChannel = null;
@@ -8,10 +18,9 @@ window.selectedSoundChannel = null;
* @param {SoundChannel[]} vv Sound channel data to populate the table. * @param {SoundChannel[]} vv Sound channel data to populate the table.
*/ */
function fill_soundchanneltablebody(vv) { function fill_soundchanneltablebody(vv) {
let $tbody = $('#soundchanneltablebody');
let $btnEditSoundChannel = $('#btnEditSoundChannel'); let $btnEditSoundChannel = $('#btnEditSoundChannel');
let $tablesizeSoundChannel = $('#tablesizeSoundChannel'); let $tablesizeSoundChannel = $('#tablesizeSoundChannel');
$tbody.empty(); $('#soundchanneltablebody').empty();
$tablesizeSoundChannel.text('Table Length : N/A'); $tablesizeSoundChannel.text('Table Length : N/A');
if (!Array.isArray(vv) || vv.length === 0) return; if (!Array.isArray(vv) || vv.length === 0) return;
@@ -22,9 +31,9 @@ function fill_soundchanneltablebody(vv) {
<td>${item.channel}</td> <td>${item.channel}</td>
<td>${item.ip}</td> <td>${item.ip}</td>
</tr>`; </tr>`;
$tbody.append(row); $('#soundchanneltablebody').append(row);
let $addedrow = $('#soundchanneltablebody tr:last'); let $addedrow = $('#soundchanneltablebody tr:last');
$addedrow.click(function () { $addedrow.off('click').on('click', function () {
if (selectedSoundChannel) { if (selectedSoundChannel) {
selectedSoundChannel.find('td').css('background-color', ''); selectedSoundChannel.find('td').css('background-color', '');
if (selectedSoundChannel.is($(this))) { if (selectedSoundChannel.is($(this))) {
@@ -41,7 +50,22 @@ function fill_soundchanneltablebody(vv) {
$tablesizeSoundChannel.text("Table Size: " + vv.length); $tablesizeSoundChannel.text("Table Size: " + vv.length);
} }
/**
* Reload sound channels from server
* @param {String} APIURL API URL endpoint (default "SoundChannel/")
*/
function reloadSoundChannel(APIURL = "SoundChannel/") {
window.soundChannels = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadSoundChannel : ", okdata)
window.soundChannels.push(...okdata);
fill_soundchanneltablebody(window.soundChannels);
} else console.log("reloadSoundChannel: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
$(document).ready(function () { $(document).ready(function () {
console.log("soundchannel.js loaded successfully"); console.log("soundchannel.js loaded successfully");
@@ -58,7 +82,7 @@ $(document).ready(function () {
$btnEditSoundChannel.prop('disabled', true); $btnEditSoundChannel.prop('disabled', true);
let API_SoundChannel = "SoundChannel/"; let API_SoundChannel = "SoundChannel/";
$findsoundchannel.on('input', function () { $findsoundchannel.off('input').on('input', function () {
let searchTerm = $(this).val().toLowerCase(); let searchTerm = $(this).val().toLowerCase();
if (searchTerm.length==0){ if (searchTerm.length==0){
window.selectedSoundChannel = null; window.selectedSoundChannel = null;
@@ -83,20 +107,16 @@ $(document).ready(function () {
$soundchannelip.val(''); $soundchannelip.val('');
} }
reloadSoundChannel(API_SoundChannel, () => { reloadSoundChannel(API_SoundChannel);
fill_soundchanneltablebody(window.soundChannels); $btnReinitializeSoundChannel.off('click').on('click', () => {
});
$btnReinitializeSoundChannel.click(() => {
DoClear(API_SoundChannel, "SoundChannels", (okdata) => { DoClear(API_SoundChannel, "SoundChannels", (okdata) => {
reloadSoundChannel(API_SoundChannel, () => { reloadSoundChannel(API_SoundChannel);
fill_soundchanneltablebody(window.soundChannels); alert("Success clear sound channels: " + okdata.message);
alert("Success clear sound channels: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error clear sound channels: " + errdata.message); alert("Error clear sound channels: " + errdata.message);
}); });
}); });
$btnEditSoundChannel.click(() => { $btnEditSoundChannel.off('click').on('click', () => {
if (selectedSoundChannel) { if (selectedSoundChannel) {
let cells = selectedSoundChannel.find('td'); let cells = selectedSoundChannel.find('td');
/** @type {SoundChannel} */ /** @type {SoundChannel} */
@@ -135,10 +155,8 @@ $(document).ready(function () {
fetchAPI(API_SoundChannel + "UpdateByIndex/" + newsc.index, "PATCH", {}, newsc, (okdata) => { fetchAPI(API_SoundChannel + "UpdateByIndex/" + newsc.index, "PATCH", {}, newsc, (okdata) => {
reloadSoundChannel(API_SoundChannel, () => { reloadSoundChannel(API_SoundChannel);
fill_soundchanneltablebody(window.soundChannels); alert("Success edit sound channel: " + okdata.message);
alert("Success edit sound channel: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error edit sound channel: " + errdata.message); alert("Error edit sound channel: " + errdata.message);
}); });
@@ -152,16 +170,14 @@ $(document).ready(function () {
} }
} }
}); });
$btnExportSoundChannel.click(() => { $btnExportSoundChannel.off('click').on('click', () => {
DoExport(API_SoundChannel, "soundchannels.xlsx", {}); DoExport(API_SoundChannel, "soundchannels.xlsx", {});
}); });
$btnImportSoundChannel.click(() => { $btnImportSoundChannel.off('click').on('click', () => {
DoImport(API_SoundChannel, (okdata) => { DoImport(API_SoundChannel, (okdata) => {
reloadSoundChannel(API_SoundChannel, () => { reloadSoundChannel(API_SoundChannel);
fill_soundchanneltablebody(window.soundChannels); alert("Success import sound channels: " + okdata.message);
alert("Success import sound channels: " + okdata.message);
});
}, (errdata) => { }, (errdata) => {
alert("Error importing sound channels from XLSX: " + errdata.message); alert("Error importing sound channels from XLSX: " + errdata.message);
}); });

View File

@@ -4,9 +4,10 @@
* @property {string} username Username * @property {string} username Username
* @property {string} password Password (plain) * @property {string} password Password (plain)
* @property {string} location Location * @property {string} location Location
* @property {string} soundbank_tags Soundbank variable tags separated by semicolon ; * @property {string} airline_tags Airline variable tags (string) separated by semicolon ;
* @property {string} messagebank_ann_id Messagebank announcement ID separated by semicolon ; * @property {string} city_tags City variable tags (string) separated by semicolon ;
* @property {string} broadcastzones Broadcast zones separated by semicolon ; * @property {string} messagebank_ann_id Messagebank announcement ID (number) separated by semicolon ;
* @property {string} broadcastzones Broadcast zones description (string) separated by semicolon ;
*/ */
/** List of UserDB data loaded from server /** List of UserDB data loaded from server
@@ -20,6 +21,89 @@ window.userdb = [];
*/ */
window.selecteduserrow = null; window.selecteduserrow = null;
/**
* @typedef {Object} KeyValueMessage
* @property {string} key
* @property {string} value
*/
/**
* List of airline tags loaded from server
* @type {KeyValueMessage[]}
*/
window.airlinetags = [];
/**
* List of city tags loaded from server
* @type {KeyValueMessage[]}
*/
window.citytags = [];
/**
* List of message bank IDs loaded from server
* @type {KeyValueMessage[]}
*/
window.messagebankids = [];
/**
* List of broadcast zones description loaded from server
* @type {string[]}
*/
window.broadcastzones = [];
/**
* Get Messagebank ANN_IDs from server
*/
function get_messagebankids() {
window.messagebankids = [];
fetchAPI("MessageBank/" + "MessageIDs", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.messagebankids.push(...okdata);
}
}, (errdata) => {
alert("Error loading message bank IDs : " + errdata.message);
});
}
/**
* Get Airline Tags from server
*/
function get_airlinetags() {
window.airlinetags = [];
fetchAPI("SoundBank/" + "AirlineTags", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.airlinetags.push(...okdata);
}
}, (errdata) => {
alert("Error loading airline tags : " + errdata.message);
});
}
/**
* Get City Tags from server
*/
function get_citytags() {
window.citytags = [];
fetchAPI("SoundBank/" + "CityTags", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.citytags.push(...okdata);
}
}, (errdata) => {
alert("Error loading city tags : " + errdata.message);
});
}
/**
* Get Broadcast Zones descriptions from server
*/
function get_broadcastzones_descriptions() {
window.broadcastzones = [];
fetchAPI("BroadcastZones/" + "BroadcastZoneDescriptions", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.broadcastzones.push(...okdata);
}
}, (errdata) => {
alert("Error loading broadcast zones : " + errdata.message);
});
}
/** /**
* Fill user table body with values * Fill user table body with values
* @param {UserDB[]} vv values to fill * @param {UserDB[]} vv values to fill
@@ -34,14 +118,16 @@ function fill_usertablebody(vv) {
vv.forEach(item => { vv.forEach(item => {
const row = `<tr> const row = `<tr>
<td>${item.index}</td> <td>${item.index}</td>
<td>${item.datenya}</td> <td>${item.username}</td>
<td>${item.timenya}</td> <td>${item.location}</td>
<td>${item.machine}</td> <td>${item.airline_tags}</td>
<td>${item.description}</td> <td>${item.city_tags}</td>
<td>${item.messagebank_ann_id}</td>
<td>${item.broadcastzones}</td>
</tr>`; </tr>`;
$('#usertablebody').append(row); $('#usertablebody').append(row);
let $addedrow = $('#usertablebody tr:last'); let $addedrow = $('#usertablebody tr:last');
$addedrow.on('click', function () { $addedrow.off('click').on('click', function () {
if (window.selecteduserrow) { if (window.selecteduserrow) {
window.selecteduserrow.find('td').css('background-color', ''); window.selecteduserrow.find('td').css('background-color', '');
if (window.selecteduserrow.is($(this))) { if (window.selecteduserrow.is($(this))) {
@@ -63,9 +149,9 @@ function fill_usertablebody(vv) {
/** /**
* Reload UserDB from server with date and filter * Reload UserDB from server with date and filter
* @param {String} APIURL API URL endpoint , default "User/" * @param {String} APIURL API URL endpoint , default "UserManagement/"
*/ */
function reloaduserDB(APIURL = "User/") { function reloaduserDB(APIURL = "UserManagement/") {
window.userdb = []; window.userdb = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => { fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) { if (Array.isArray(okdata)) {
@@ -79,50 +165,395 @@ function reloaduserDB(APIURL = "User/") {
$(document).ready(function () { $(document).ready(function () {
console.log("usermanagement.js ready"); console.log("usermanagement.js ready");
let $usertablebody = $('#usertablebody'); get_airlinetags();
let $finduser = $('#finduser'); get_citytags();
let $btnClear = $('#btnClear'); get_messagebankids();
let $btnAdd = $('#btnAdd'); get_broadcastzones_descriptions();
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
let APIURL = "User/";
// add / edit modal elements let APIURL = "UserManagement/";
let $addmodal = $('#addmodal');
let $modalindex = $('#modalindex');
let $modalusername = $('#modalusername');
let $modalpassword = $('#modalpassword');
let $modalverifypassword = $('#modalverifypassword');
let $modalsoundbank = $('#modalsoundbank');
let $modalmessagebank = $('#modalmessagebank');
let $modalbroadcastzones = $('#modalbroadcastzones');
let $btnShowSoundbankModal = $('#btnShowSoundbankModal');
let $btnShowMessagebankModal = $('#btnShowMessagebankModal');
let $btnShowBroaadcastZoneModal = $('#btnShowBroaadcastZoneModal');
let $usermanagementsave = $('#usermanagementsave');
let $usermanagementclose = $('#usermanagementclose');
// soundbank selection modal elements function clearAddModal() {
let $soundbankmodal = $('#soundbankmodal'); $('#modalindex').val("");
let $soundbankselection = $('#soundbankselection'); $('#modalusername').val("");
let $soundbankselectionsave = $('#soundbankselectionsave'); $('#modalpassword').val("");
let $soundbankselectionclose = $('#soundbankselectionclose'); $('#modalverifypassword').val("");
$('#modalairlinetags').val("");
$('#modalcitytags').val("");
$('#modalmessagebank').val("");
$('#modalbroadcastzones').val("");
$('#modallocation').val("");
}
function fill_citylist() {
$('#citylist').empty();
citytags.forEach(tag => {
let value = `${tag.value} [${tag.key}]`;
const row = `<div class="form-check">
<input class="form-check-input citytagcheckbox" type="checkbox" value="${tag.key}" id="citytag_${tag.key}">
<label class="form-check-label" for="citytag_${tag.key}">
${value}
</label>
</div>`;
$('#citylist').append(row);
});
}
function fill_airlinelist() {
$('#airlinelist').empty();
airlinetags.forEach(tag => {
let value = `${tag.value} [${tag.key}]`;
const row = `<div class="form-check">
<input class="form-check-input airlinetagcheckbox" type="checkbox" value="${tag.key}" id="airlinetag_${tag.key}">
<label class="form-check-label" for="airlinetag_${tag.key}">
${value}
</label>
</div>`;
$('#airlinelist').append(row);
});
}
// broadcast zone selection modal elements // broadcast zone selection modal elements
let $broadcastzonemodal = $('#broadcastzonemodal');
let $broadcastzoneselection = $('#broadcastzoneselection'); function fill_broadcastzonelist() {
let $broadcastzoneselectionsave = $('#broadcastzoneselectionsave'); $('#broadcastzonelist').empty();
let $broadcastzoneselectionclose = $('#broadcastzoneselectionclose'); broadcastzones.forEach(desc => {
const row = `<div class="form-check">
<input class="form-check-input broadcastzonecheckbox" type="checkbox" value="${desc}" id="broadcastzone_${desc}">
<label class="form-check-label" for="broadcastzone_${desc}">
${desc}
</label>
</div>`;
$('#broadcastzonelist').append(row);
});
}
// messagebank selection modal elements // messagebank selection modal elements
let $messagebankmodal = $('#messagebankmodal');
let $messagebankselection = $('#messagebankselection');
let $messagebankselectionsave = $('#messagebankselectionsave');
let $messagebankselectionclose = $('#messagebankselectionclose');
$usertablebody.empty(); function fill_messagebanklist() {
$finduser.on('input', function () { $('#messagebanklist').empty();
messagebankids.forEach(id => {
let value = `${id.value} [${id.key}]`;
const row = `<div class="form-check">
<input class="form-check-input messagebankidcheckbox" type="checkbox" value="${id.key}" id="messagebankid_${id.key}">
<label class="form-check-label" for="messagebankid_${id.key}">
${value}
</label>
</div>`;
$('#messagebanklist').append(row);
});
}
$('#usertablebody').empty();
reloaduserDB();
$('#finduser').off('input').on('input', function () {
let searchTerm = $(this).val().toLowerCase();
if (searchTerm.length > 0) {
let filteredUsers = window.userdb.filter(user =>
user.username.toLowerCase().includes(searchTerm) ||
user.airline_tags.toLowerCase().includes(searchTerm) ||
user.city_tags.toLowerCase().includes(searchTerm)
//user.messagebank_ann_id.toLowerCase().includes(searchTerm) ||
//user.broadcastzones.toLowerCase().includes(searchTerm)
);
fill_usertablebody(filteredUsers);
} else {
fill_usertablebody(window.userdb);
}
});
/**
* Show modal dialog for soundbank, messagebank, broadcastzone selection
* @param {boolean} editmode if true, edit mode, else add mode
* @param {number} index index of user to edit, default 0
*/
function modalshow(editmode = false, index=0) {
// event on click btnShowSoundbankModal
$('#btnShowSoundbankModal').off('click').on('click', function () {
$('#soundbankmodal').modal('show');
fill_citylist();
fill_airlinelist();
let airline = $('#modalairlinetags').val().trim();
let city = $('#modalcitytags').val().trim();
if (airline.length > 0) {
let airlinekeys = airline.split(";");
$('#airlinelist input[type=checkbox]').each(function () {
let tag = $(this).val();
if (airlinekeys.includes(tag)) {
$(this).prop('checked', true);
}
});
}
if (city.length > 0) {
let citykeys = city.split(";");
$('#citylist input[type=checkbox]').each(function () {
let tag = $(this).val();
if (citykeys.includes(tag)) {
$(this).prop('checked', true);
}
});
}
$('#soundbankmodal').off('click.soundbankselectionsave').on('click.soundbankselectionsave', '#soundbankselectionsave', function () {
let selected_airlinetags = [];
$('#airlinelist input[type=checkbox]:checked').each(function () {
selected_airlinetags.push($(this).val());
});
let selected_citytags = [];
$('#citylist input[type=checkbox]:checked').each(function () {
selected_citytags.push($(this).val());
});
//console.log("Selected airline tags: ", selected_airlinetags);
//console.log("Selected city tags: ", selected_citytags);
if (selected_airlinetags.length == 0 || selected_citytags.length == 0) {
alert("Please select at least one airline tag and one city tag.");
return;
}
let airlinevalue = selected_airlinetags.join(";");
let cityvalue = selected_citytags.join(";");
$('#modalairlinetags').val(airlinevalue);
$('#modalcitytags').val(cityvalue);
$('#soundbankmodal').modal('hide');
});
$('#soundbankmodal').off('click.soundbankselectionclose').on('click.soundbankselectionclose', '#soundbankselectionclose', function () {
$('#soundbankmodal').modal('hide');
});
});
// event on click btnShowMessagebankModal
$('#btnShowMessagebankModal').off('click').on('click', function () {
$('#messagebankmodal').modal('show');
fill_messagebanklist();
let messagebank = $('#modalmessagebank').val().trim();
if (messagebank.length > 0) {
let messagebankkeys = messagebank.split(";");
$('#messagebanklist input[type=checkbox]').each(function () {
let id = $(this).val();
if (messagebankkeys.includes(id)) {
$(this).prop('checked', true);
}
});
}
$('#messagebankmodal').off('click.messagebankselectionsave').on('click.messagebankselectionsave', '#messagebankselectionsave', function () {
let selected_messagebankids = [];
$('#messagebanklist input[type=checkbox]:checked').each(function () {
selected_messagebankids.push($(this).val());
});
//console.log("Selected message bank IDs: ", selected_messagebankids);
if (selected_messagebankids.length == 0) {
alert("Please select at least one message bank ID.");
return;
}
let messagebankvalue = selected_messagebankids.join(";");
$('#modalmessagebank').val(messagebankvalue);
$('#messagebankmodal').modal('hide');
});
$('#messagebankmodal').off('click.messagebankselectionclose').on('click.messagebankselectionclose', '#messagebankselectionclose', function () {
$('#messagebankmodal').modal('hide');
});
});
// event on click btnShowBroaadcastZoneModal
$('#btnShowBroaadcastZoneModal').off('click').on('click', function () {
$('#broadcastzonemodal').modal('show');
fill_broadcastzonelist();
let broadcastzones = $('#modalbroadcastzones').val().trim();
if (broadcastzones.length > 0) {
let broadcastzonesvalues = broadcastzones.split(";");
$('#broadcastzonelist input[type=checkbox]').each(function () {
let desc = $(this).val();
if (broadcastzonesvalues.includes(desc)) {
$(this).prop('checked', true);
}
});
}
$('#broadcastzonemodal').off('click.broadcastzoneselectionsave').on('click.broadcastzoneselectionsave', '#broadcastzoneselectionsave', function () {
let selected_broadcastzones = [];
$('#broadcastzonelist input[type=checkbox]:checked').each(function () {
selected_broadcastzones.push($(this).val());
});
//console.log("Selected broadcast zones: ", selected_broadcastzones);
if (selected_broadcastzones.length == 0) {
alert("Please select at least one broadcast zone.");
return;
}
let broadcastzonesvalue = selected_broadcastzones.join(";");
$('#modalbroadcastzones').val(broadcastzonesvalue);
$('#broadcastzonemodal').modal('hide');
});
$('#broadcastzonemodal').off('click.broadcastzoneselectionclose').on('click.broadcastzoneselectionclose', '#broadcastzoneselectionclose', function () {
$('#broadcastzonemodal').modal('hide');
});
});
// event on Click save button
$('#addmodal').off('click.usermanagementsave').on('click.usermanagementsave', '#usermanagementsave', function () {
let username = $('#modalusername').val().trim();
let password = $('#modalpassword').val();
let verifypassword = $('#modalverifypassword').val();
let location = $('#modallocation').val().trim();
let airline_tags = $('#modalairlinetags').val().trim();
let city_tags = $('#modalcitytags').val().trim();
let messagebank_ann_id = $('#modalmessagebank').val().trim();
let broadcastzones = $('#modalbroadcastzones').val().trim();
if (username.length === 0) {
alert("Username cannot be empty");
return;
}
if (password.length === 0 || verifypassword.length === 0) {
alert("Password cannot be empty");
return;
}
if (password !== verifypassword) {
alert("Password and Verify Password do not match");
return;
}
if (airline_tags.length === 0) {
alert("Airline tags cannot be empty");
return;
}
if (city_tags.length === 0) {
alert("City tags cannot be empty");
return;
}
if (messagebank_ann_id.length === 0) {
alert("Message bank ANN_ID cannot be empty");
return;
}
if (broadcastzones.length === 0) {
alert("Broadcast zones cannot be empty");
return;
}
/**
* @type {UserDB}
*/
let ll = {
index: index,
username: username,
password: password,
location: location,
airline_tags: airline_tags,
city_tags: city_tags,
messagebank_ann_id: messagebank_ann_id,
broadcastzones: broadcastzones
}
if (editmode) {
fetchAPI(APIURL + "UpdateByIndex/" + index, "PATCH", {}, ll, (okdata) => {
alert("Success update User : " + okdata.message);
reloaduserDB();
}, (errdata) => {
alert("Error update User : " + errdata.message);
});
} else {
fetchAPI(APIURL + "Add", "POST", {}, ll, (okdata) => {
alert("Success add User : " + okdata.message);
reloaduserDB();
}, (errdata) => {
alert("Error add User : " + errdata.message);
});
}
$('#addmodal').modal('hide');
});
// event on Click close button
$('#addmodal').off('click.usermanagementclose').on('click.usermanagementclose', '#usermanagementclose', function () {
$('#addmodal').modal('hide');
});
}
$('#btnClear').off('click').on('click', function () {
DoClear(APIURL, "UserManagement", (okdata) => {
reloaduserDB();
alert("Success clear user management : " + okdata.message);
}, (errdata) => {
alert("Error clear user management : " + errdata.message);
});
});
$('#btnAdd').off('click').on('click', () => {
$('#addmodal').modal('show');
clearAddModal();
modalshow(false,0);
});
$('#btnRemove').off('click').on('click', () => {
if (window.selecteduserrow) {
let cells = window.selecteduserrow.find('td');
/** @type {UserDB} */
let user = {
index: parseInt(cells.eq(0).text()),
username: cells.eq(1).text(),
password: cells.eq(2).text(),
airline_tags: cells.eq(3).text(),
city_tags: cells.eq(4).text(),
messagebank_ann_id: cells.eq(5).text(),
broadcastzones: cells.eq(6).text()
}
if (confirm(`Are you sure to delete user [${user.index}] Username=${user.username} ?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + user.index, "DELETE", {}, null, (okdata) => {
reloaduserDB();
alert("Success delete user : " + okdata.message);
}, (errdata) => {
alert("Error delete user : " + errdata.message);
});
}
} else {
alert("No user selected");
}
});
$('#btnEdit').off('click').on('click', () => {
if (window.selecteduserrow) {
let cells = window.selecteduserrow.find('td');
let index = parseInt(cells.eq(0).text());
if (isNaN(index) || index <= 0) {
alert("Invalid user index");
return;
}
/** @type {UserDB} */
let user = window.userdb.find(u => u.index === index);
if (!user) {
alert("User not found");
return;
}
if (confirm(`Are you sure to edit user [${user.index}] Username=${user.username} ?`)) {
$('#addmodal').modal('show');
// fill modal with user data
$('#modalindex').val(user.index);
$('#modalusername').val(user.username);
$('#modalpassword').val(user.password);
$('#modalverifypassword').val(user.password);
$('#modallocation').val(user.location);
$('#modalairlinetags').val(user.airline_tags);
$('#modalcitytags').val(user.city_tags);
$('#modalmessagebank').val(user.messagebank_ann_id);
$('#modalbroadcastzones').val(user.broadcastzones);
modalshow(true, user.index);
}
} else {
alert("No user selected");
}
});
$('#btnExport').off('click').on('click', () => {
DoExport(APIURL, "user.xlsx", {});
});
$('#btnImport').off('click').on('click', () => {
DoImport(APIURL, (okdata) => {
reloaduserDB();
alert("Success import user : " + okdata.message);
}, (errdata) => {
alert("Error importing user from XLSX : " + errdata.message);
});
}); });
}); });

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -19,15 +20,15 @@
<div class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal"> <div class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Broadcast Zones</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button> <h4 class="modal-title">Add / Edit Broadcast Zones</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p> <p class="text-add">Index</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 form-control input-add" type="text" id="broadcastzoneindex" placeholder="index"></div> <div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 form-control input-add" type="text" id="broadcastzoneindex" placeholder="index" readonly=""></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
@@ -79,7 +80,7 @@
<div class="form-check"><input class="form-check-input" type="checkbox" id="R08"><label class="form-check-label" for="formCheck-8">08</label></div> <div class="form-check"><input class="form-check-input" type="checkbox" id="R08"><label class="form-check-label" for="formCheck-8">08</label></div>
</div> </div>
</div> </div>
<div class="col-3"> <div class="col-3 invisible">
<div class="row"> <div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R09"><label class="form-check-label" for="formCheck-25">09</label></div> <div class="form-check"><input class="form-check-input" type="checkbox" id="R09"><label class="form-check-label" for="formCheck-25">09</label></div>
</div> </div>
@@ -105,7 +106,7 @@
<div class="form-check"><input class="form-check-input" type="checkbox" id="R16"><label class="form-check-label" for="formCheck-32">16</label></div> <div class="form-check"><input class="form-check-input" type="checkbox" id="R16"><label class="form-check-label" for="formCheck-32">16</label></div>
</div> </div>
</div> </div>
<div class="col-3"> <div class="col-3 invisible">
<div class="row"> <div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R17"><label class="form-check-label" for="formCheck-17">17</label></div> <div class="form-check"><input class="form-check-input" type="checkbox" id="R17"><label class="form-check-label" for="formCheck-17">17</label></div>
</div> </div>
@@ -131,7 +132,7 @@
<div class="form-check"><input class="form-check-input" type="checkbox" id="R24"><label class="form-check-label" for="formCheck-24">24</label></div> <div class="form-check"><input class="form-check-input" type="checkbox" id="R24"><label class="form-check-label" for="formCheck-24">24</label></div>
</div> </div>
</div> </div>
<div class="col-3"> <div class="col-3 invisible">
<div class="row"> <div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R25"><label class="form-check-label" for="formCheck-9">25</label></div> <div class="form-check"><input class="form-check-input" type="checkbox" id="R25"><label class="form-check-label" for="formCheck-9">25</label></div>
</div> </div>
@@ -194,9 +195,9 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th class="col-sm-3">Description</th> <th class="class75">Description</th>
<th class="col">IP Address</th> <th class="class20">IP Address</th>
</tr> </tr>
</thead> </thead>
<tbody id="soundchanneltablebody"></tbody> <tbody id="soundchanneltablebody"></tbody>
@@ -235,11 +236,11 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th class="col-sm-2">Description</th> <th class="class40">Description</th>
<th class="col-sm-2">SoundChannel</th> <th class="class20">SoundChannel</th>
<th class="col-sm-2">ID</th> <th class="class05">ID</th>
<th class="col">BP</th> <th class="class30">BP</th>
</tr> </tr>
</thead> </thead>
<tbody id="broadcastzonetablebody"></tbody> <tbody id="broadcastzonetablebody"></tbody>
@@ -254,10 +255,10 @@
<div class="modal fade" role="dialog" tabindex="-1" id="soundchannelmodal"> <div class="modal fade" role="dialog" tabindex="-1" id="soundchannelmodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header bg-header-modal">
<h4 class="modal-title">Edit Sound Channel</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button> <h4 class="modal-title">Edit Sound Channel</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p> <p class="text-add">Index</p>
@@ -268,13 +269,13 @@
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p> <p class="text-add">Description</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-100 form-control input-add" type="text" id="soundchanneldescription" placeholder="Description"></div> <div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-100 form-control input-add" type="text" id="soundchanneldescription" placeholder="Description" readonly=""></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">IP Address</p> <p class="text-add">IP Address</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-100 form-control input-add" type="text" id="soundchannelip" placeholder="IP Address"></div> <div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-100 form-control input-add" type="text" id="soundchannelip" placeholder="IP Address" required="" inputmode="numeric" pattern="^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$"></div>
</div> </div>
</div> </div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundchannelclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundchannelsave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundchannelclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundchannelsave" type="button">Save</button></div>
@@ -282,6 +283,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/soundchannel.js"></script> <script src="assets/js/soundchannel.js"></script>
<script src="assets/js/broadcastzones.js"></script> <script src="assets/js/broadcastzones.js"></script>
</body> </body>

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS NewGeneration 17092025</title> <title>AAS NewGeneration 17092025</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/select2.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -97,9 +99,10 @@
</div> </div>
<div class="container w-100 pad-container" id="content"></div> <div class="container w-100 pad-container" id="content"></div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/jquery-3.7.1.min.js"></script> <script src="assets/js/jquery-3.7.1.min.js"></script>
<script src="assets/js/select2.js"></script>
<script src="assets/js/script.js"></script> <script src="assets/js/script.js"></script>
<script src="assets/js/select2.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -41,9 +42,9 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th class="col-sm-2">TAG</th> <th class="class20">TAG</th>
<th class="col">Languages</th> <th class="class75">Languages</th>
</tr> </tr>
</thead> </thead>
<tbody id="languagebanktablebody"></tbody> <tbody id="languagebanktablebody"></tbody>
@@ -53,10 +54,10 @@
<div class="modal fade" role="dialog" tabindex="-1" id="languagemodal"> <div class="modal fade" role="dialog" tabindex="-1" id="languagemodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Language</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button> <h4 class="modal-title">Add / Edit Language</h4><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p> <p class="text-add">Index</p>
@@ -88,6 +89,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/languagelink.js"></script> <script src="assets/js/languagelink.js"></script>
</body> </body>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -38,11 +39,11 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-1 col-md-1">No</th> <th class="class10">No</th>
<th class="col-2 col-md-2 col-lg-2">Date</th> <th class="class15">Date</th>
<th class="col-2 col-md-2 col-lg-2">Time</th> <th class="class15">Time</th>
<th class="col-2 col-md-2 col-lg-2">Machine</th> <th class="class15">Machine</th>
<th>Description</th> <th class="class45">Description</th>
</tr> </tr>
</thead> </thead>
<tbody id="logtablebody"></tbody> <tbody id="logtablebody"></tbody>
@@ -50,6 +51,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/log.js"></script> <script src="assets/js/log.js"></script>
</body> </body>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS NewGeneration 17092025</title> <title>AAS NewGeneration 17092025</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -13,22 +14,21 @@
<body> <body>
<section class="position-relative py-4 py-xl-5"> <section class="position-relative py-4 py-xl-5">
<div class="container"> <div class="container">
<div class="row mb-5"> <div class="row mb-4"></div>
<div class="col-md-8 col-xl-6 text-center mx-auto"> <div class="row d-flex justify-content-center mb-7">
<h2>Sign In</h2>
</div>
</div>
<div class="row d-flex justify-content-center">
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
<div class="card mb-5 card-login"> <div class="card mb-5 card-login">
<div class="card-body d-flex flex-column align-items-center"> <div class="card-body d-flex flex-column align-items-center">
<div class="bs-icon-xl bs-icon-circle bs-icon-primary my-4 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-person bg-icon-login"> <div class="bs-icon-xl bs-icon-circle bs-icon-primary my-4 bs-icon bg-icon-login"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-person bg-icon-login">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664z"></path> <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664z"></path>
</svg></div> </svg></div>
<form class="text-center" method="post"> <h2 class="mb-3 h-login">Login</h2>
<div class="mb-3"><input class="form-control input-login" type="text" name="username" placeholder="Username"></div> <form class="text-center py-2 bottom-signin" method="post">
<div class="mb-3"><input class="form-control input-login" type="password" name="password" placeholder="Password"></div> <p class="p-login">Username</p>
<div class="mb-3"><button class="btn btn-primary d-block w-100 btn-login" type="submit">Sign In</button></div> <div class="mb-3"><input class="form-control input-login" type="text" name="username" placeholder="Enter your username"></div>
<p class="p-login">Password</p>
<div class="mb-3"><input class="form-control input-login" type="password" name="password" placeholder="Enter your password"></div>
<div class="mb-3 py-2"><button class="btn btn-primary d-block w-100 btn-login" type="submit">Login</button></div>
</form> </form>
</div> </div>
</div> </div>
@@ -37,6 +37,7 @@
</div> </div>
</section> </section>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
</body> </body>
</html> </html>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -41,13 +42,13 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th class="col-sm-2">Description</th> <th class="class15">Description</th>
<th class="col-sm-1">Language</th> <th class="class10">Language</th>
<th class="col-sm-1">ANN ID</th> <th class="class10">ANN ID</th>
<th class="col-sm-1">Type</th> <th class="class10">Type</th>
<th class="col-sm-3">Message Details</th> <th class="class35">Message Details</th>
<th class="col-sm-3">Message Tags</th> <th class="class15">Message Tags</th>
</tr> </tr>
</thead> </thead>
<tbody id="messagebanktablebody"></tbody> <tbody id="messagebanktablebody"></tbody>
@@ -55,29 +56,29 @@
</div> </div>
</div> </div>
<div class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal"> <div class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog modal-xl" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Message</h4> <h4 class="modal-title">Add / Edit Message</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Index</p> <p class="text-add">Index</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 input-add form-control" type="text" id="messageindex" placeholder="Index" readonly=""></div> <div class="col-8 col-sm-8 col-md-8 col-lg-9 col-xl-9"><input class="w-25 input-add form-control" type="text" id="messageindex" placeholder="Index" readonly=""></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Description</p> <p class="text-add">Description</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="messagedescription" class="input-add form-control" placeholder="Description"></div> <div class="col-8 col-sm-8 col-md-8 col-lg-9 col-xl-9"><input type="text" id="messagedescription" class="input-add form-control" placeholder="Description"></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Language</p> <p class="text-add">Language</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="messagelanguage" class="input-add form-control"> <div class="col-8 col-sm-8 col-md-8 col-lg-9 col-xl-9"><select id="messagelanguage" class="input-add form-control">
<option value="INDONESIA">Indonesia</option> <option value="INDONESIA">Indonesia</option>
<option value="LOCAL">Local</option> <option value="LOCAL">Local</option>
<option value="ENGLISH">English</option> <option value="ENGLISH">English</option>
@@ -87,40 +88,40 @@
</select></div> </select></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">ANN ID</p> <p class="text-add">ANN ID</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="number" id="messageannid" class="input-add form-control" min="1" max="100" value="1" step="1" placeholder="Announcement ID"></div> <div class="col-8 col-sm-8 col-md-8 col-lg-9 col-xl-9"><input type="number" id="messageannid" class="input-add form-control" min="1" max="100" value="1" step="1" placeholder="Announcement ID"></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Voice Type</p> <p class="text-add">Voice Type</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="messagevoicetype" class="input-add form-control"> <div class="col-8 col-sm-8 col-md-8 col-lg-9 col-xl-9"><select id="messagevoicetype" class="input-add form-control">
<option value="VOICE_1">Voice 1</option> <option value="VOICE_1">Voice 1</option>
<option value="VOICE_2">Voice 2</option> <option value="VOICE_2">Voice 2</option>
<option value="VOICE_3">Voice 3</option> <option value="VOICE_3">Voice 3</option>
</select></div> </select></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col bg-light">
<ul class="list-unstyled w-100 h-100" id="messageavailablevariables"></ul> <ul class="list-unstyled w-100 h-100" id="messageavailablevariables"></ul>
</div> </div>
<div class="col-2"> <div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2">
<div class="row"><button class="btn btn-danger" id="btnclearlist" type="button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" style="font-size: 32;"> <div class="row pad-row-btn"><button class="btn btn-round-basic color-remove" data-bs-toggle="tooltip" data-bss-tooltip="" id="btnclearlist" type="button" title="Clear List"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" style="font-size: 32;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path> <path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
</svg></button></div> </svg></button></div>
<div class="row"><button class="btn btn-warning" id="btnremovefromlist" type="button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" style="font-size: 32;"> <div class="row pad-row-btn"><button class="btn btn-round-basic color-edit" data-bs-toggle="tooltip" data-bss-tooltip="" data-bs-placement="right" id="btnremovefromlist" type="button" title="Remove"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" style="font-size: 32;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M48 256a208 208 0 1 1 416 0A208 208 0 1 1 48 256zm464 0A256 256 0 1 0 0 256a256 256 0 1 0 512 0zM217.4 376.9c4.2 4.5 10.1 7.1 16.3 7.1c12.3 0 22.3-10 22.3-22.3V304h96c17.7 0 32-14.3 32-32V240c0-17.7-14.3-32-32-32H256V150.3c0-12.3-10-22.3-22.3-22.3c-6.2 0-12.1 2.6-16.3 7.1L117.5 242.2c-3.5 3.8-5.5 8.7-5.5 13.8s2 10.1 5.5 13.8l99.9 107.1z"></path> <path d="M48 256a208 208 0 1 1 416 0A208 208 0 1 1 48 256zm464 0A256 256 0 1 0 0 256a256 256 0 1 0 512 0zM217.4 376.9c4.2 4.5 10.1 7.1 16.3 7.1c12.3 0 22.3-10 22.3-22.3V304h96c17.7 0 32-14.3 32-32V240c0-17.7-14.3-32-32-32H256V150.3c0-12.3-10-22.3-22.3-22.3c-6.2 0-12.1 2.6-16.3 7.1L117.5 242.2c-3.5 3.8-5.5 8.7-5.5 13.8s2 10.1 5.5 13.8l99.9 107.1z"></path>
</svg></button></div> </svg></button></div>
<div class="row"><button class="btn btn-success" id="btnaddtolist" type="button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" style="font-size: 32;"> <div class="row pad-row-btn"><button class="btn btn-round-basic color-import" data-bs-toggle="tooltip" data-bss-tooltip="" data-bs-placement="right" id="btnaddtolist" type="button" title="Add"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" style="font-size: 32;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M464 256A208 208 0 1 1 48 256a208 208 0 1 1 416 0zM0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM294.6 135.1c-4.2-4.5-10.1-7.1-16.3-7.1C266 128 256 138 256 150.3V208H160c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32h96v57.7c0 12.3 10 22.3 22.3 22.3c6.2 0 12.1-2.6 16.3-7.1l99.9-107.1c3.5-3.8 5.5-8.7 5.5-13.8s-2-10.1-5.5-13.8L294.6 135.1z"></path> <path d="M464 256A208 208 0 1 1 48 256a208 208 0 1 1 416 0zM0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM294.6 135.1c-4.2-4.5-10.1-7.1-16.3-7.1C266 128 256 138 256 150.3V208H160c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32h96v57.7c0 12.3 10 22.3 22.3 22.3c6.2 0 12.1-2.6 16.3-7.1l99.9-107.1c3.5-3.8 5.5-8.7 5.5-13.8s-2-10.1-5.5-13.8L294.6 135.1z"></path>
</svg></button></div> </svg></button></div>
</div> </div>
<div class="col"> <div class="col bg-light">
<ul class="list-unstyled w-100 h-100" id="messageselectedvariables"></ul> <ul class="list-unstyled w-100 h-100" id="messageselectedvariables"></ul>
</div> </div>
</div> </div>
@@ -130,6 +131,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/messagebank.js"></script> <script src="assets/js/messagebank.js"></script>
</body> </body>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -101,7 +102,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card">
<div class="card" id="streamercard-4"> <div class="card card-channel" id="streamercard-4">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle05">Channel 05</h4> <h4 class="card-title" id="streamertitle05">Channel 05</h4>
<div class="row"> <div class="row">
@@ -120,7 +121,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card">
<div class="card" id="streamercard-5"> <div class="card card-channel" id="streamercard-5">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle06">Channel 06</h4> <h4 class="card-title" id="streamertitle06">Channel 06</h4>
<div class="row"> <div class="row">
@@ -139,7 +140,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card">
<div class="card" id="streamercard-6"> <div class="card card-channel" id="streamercard-6">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle07">Channel 07</h4> <h4 class="card-title" id="streamertitle07">Channel 07</h4>
<div class="row"> <div class="row">
@@ -158,7 +159,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 pad-card">
<div class="card" id="streamercard-7"> <div class="card card-channel" id="streamercard-7">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle08">Channel 08</h4> <h4 class="card-title" id="streamertitle08">Channel 08</h4>
<div class="row"> <div class="row">
@@ -179,7 +180,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-8"> <div class="card card-channel" id="streamercard-8">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle09">Channel 09</h4> <h4 class="card-title" id="streamertitle09">Channel 09</h4>
<div class="row"> <div class="row">
@@ -198,7 +199,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-9"> <div class="card card-channel" id="streamercard-9">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle10">Channel 10</h4> <h4 class="card-title" id="streamertitle10">Channel 10</h4>
<div class="row"> <div class="row">
@@ -217,7 +218,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-10"> <div class="card card-channel" id="streamercard-10">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle11">Channel 11</h4> <h4 class="card-title" id="streamertitle11">Channel 11</h4>
<div class="row"> <div class="row">
@@ -236,7 +237,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-11"> <div class="card card-channel" id="streamercard-11">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle12">Channel 12</h4> <h4 class="card-title" id="streamertitle12">Channel 12</h4>
<div class="row"> <div class="row">
@@ -257,7 +258,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-12"> <div class="card card-channel" id="streamercard-12">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle13">Channel 13</h4> <h4 class="card-title" id="streamertitle13">Channel 13</h4>
<div class="row"> <div class="row">
@@ -276,7 +277,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-13"> <div class="card card-channel" id="streamercard-13">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle14">Channel 14</h4> <h4 class="card-title" id="streamertitle14">Channel 14</h4>
<div class="row"> <div class="row">
@@ -295,7 +296,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-14"> <div class="card card-channel" id="streamercard-14">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle15">Channel 15</h4> <h4 class="card-title" id="streamertitle15">Channel 15</h4>
<div class="row"> <div class="row">
@@ -314,7 +315,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-15"> <div class="card card-channel" id="streamercard-15">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle16">Channel 16</h4> <h4 class="card-title" id="streamertitle16">Channel 16</h4>
<div class="row"> <div class="row">
@@ -335,7 +336,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-16"> <div class="card card-channel" id="streamercard-16">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle17">Channel 17</h4> <h4 class="card-title" id="streamertitle17">Channel 17</h4>
<div class="row"> <div class="row">
@@ -354,7 +355,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-17"> <div class="card card-channel" id="streamercard-17">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle18">Channel 18</h4> <h4 class="card-title" id="streamertitle18">Channel 18</h4>
<div class="row"> <div class="row">
@@ -373,7 +374,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-18"> <div class="card card-channel" id="streamercard-18">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle19">Channel 19</h4> <h4 class="card-title" id="streamertitle19">Channel 19</h4>
<div class="row"> <div class="row">
@@ -392,7 +393,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-19"> <div class="card card-channel" id="streamercard-19">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle20">Channel 20</h4> <h4 class="card-title" id="streamertitle20">Channel 20</h4>
<div class="row"> <div class="row">
@@ -413,7 +414,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-20"> <div class="card card-channel" id="streamercard-20">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle21">Channel 21</h4> <h4 class="card-title" id="streamertitle21">Channel 21</h4>
<div class="row"> <div class="row">
@@ -432,7 +433,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-21"> <div class="card card-channel" id="streamercard-21">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle22">Channel 22</h4> <h4 class="card-title" id="streamertitle22">Channel 22</h4>
<div class="row"> <div class="row">
@@ -451,7 +452,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-22"> <div class="card card-channel" id="streamercard-22">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle23">Channel 23</h4> <h4 class="card-title" id="streamertitle23">Channel 23</h4>
<div class="row"> <div class="row">
@@ -470,7 +471,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-23"> <div class="card card-channel" id="streamercard-23">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle24">Channel 24</h4> <h4 class="card-title" id="streamertitle24">Channel 24</h4>
<div class="row"> <div class="row">
@@ -491,7 +492,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-24"> <div class="card card-channel" id="streamercard-24">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle25">Channel 25</h4> <h4 class="card-title" id="streamertitle25">Channel 25</h4>
<div class="row"> <div class="row">
@@ -510,7 +511,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-25"> <div class="card card-channel" id="streamercard-25">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle26">Channel 26</h4> <h4 class="card-title" id="streamertitle26">Channel 26</h4>
<div class="row"> <div class="row">
@@ -529,7 +530,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-26"> <div class="card card-channel" id="streamercard-26">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle27">Channel 27</h4> <h4 class="card-title" id="streamertitle27">Channel 27</h4>
<div class="row"> <div class="row">
@@ -548,7 +549,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-27"> <div class="card card-channel" id="streamercard-27">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle28">Channel 28</h4> <h4 class="card-title" id="streamertitle28">Channel 28</h4>
<div class="row"> <div class="row">
@@ -569,7 +570,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-28"> <div class="card card-channel" id="streamercard-28">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle29">Channel 29</h4> <h4 class="card-title" id="streamertitle29">Channel 29</h4>
<div class="row"> <div class="row">
@@ -588,7 +589,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-29"> <div class="card card-channel" id="streamercard-29">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle30">Channel 30</h4> <h4 class="card-title" id="streamertitle30">Channel 30</h4>
<div class="row"> <div class="row">
@@ -607,7 +608,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-30"> <div class="card card-channel" id="streamercard-30">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle31">Channel 31</h4> <h4 class="card-title" id="streamertitle31">Channel 31</h4>
<div class="row"> <div class="row">
@@ -626,7 +627,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-31"> <div class="card card-channel" id="streamercard-31">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle32">Channel 32</h4> <h4 class="card-title" id="streamertitle32">Channel 32</h4>
<div class="row"> <div class="row">
@@ -647,7 +648,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-32"> <div class="card card-channel" id="streamercard-32">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle33">Channel 33</h4> <h4 class="card-title" id="streamertitle33">Channel 33</h4>
<div class="row"> <div class="row">
@@ -666,7 +667,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-33"> <div class="card card-channel" id="streamercard-33">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle34">Channel 34</h4> <h4 class="card-title" id="streamertitle34">Channel 34</h4>
<div class="row"> <div class="row">
@@ -685,7 +686,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-34"> <div class="card card-channel" id="streamercard-34">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle35">Channel 35</h4> <h4 class="card-title" id="streamertitle35">Channel 35</h4>
<div class="row"> <div class="row">
@@ -704,7 +705,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-35"> <div class="card card-channel" id="streamercard-35">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle36">Channel 36</h4> <h4 class="card-title" id="streamertitle36">Channel 36</h4>
<div class="row"> <div class="row">
@@ -725,7 +726,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-36"> <div class="card card-channel" id="streamercard-36">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle37">Channel 37</h4> <h4 class="card-title" id="streamertitle37">Channel 37</h4>
<div class="row"> <div class="row">
@@ -744,7 +745,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-37"> <div class="card card-channel" id="streamercard-37">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle38">Channel 38</h4> <h4 class="card-title" id="streamertitle38">Channel 38</h4>
<div class="row"> <div class="row">
@@ -763,7 +764,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-38"> <div class="card card-channel" id="streamercard-38">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle39">Channel 39</h4> <h4 class="card-title" id="streamertitle39">Channel 39</h4>
<div class="row"> <div class="row">
@@ -782,7 +783,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-39"> <div class="card card-channel" id="streamercard-39">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle40">Channel 40</h4> <h4 class="card-title" id="streamertitle40">Channel 40</h4>
<div class="row"> <div class="row">
@@ -803,7 +804,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-40"> <div class="card card-channel" id="streamercard-40">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle41">Channel 41</h4> <h4 class="card-title" id="streamertitle41">Channel 41</h4>
<div class="row"> <div class="row">
@@ -822,7 +823,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-41"> <div class="card card-channel" id="streamercard-41">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle42">Channel 42</h4> <h4 class="card-title" id="streamertitle42">Channel 42</h4>
<div class="row"> <div class="row">
@@ -841,7 +842,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-42"> <div class="card card-channel" id="streamercard-42">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle43">Channel 43</h4> <h4 class="card-title" id="streamertitle43">Channel 43</h4>
<div class="row"> <div class="row">
@@ -860,7 +861,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-43"> <div class="card card-channel" id="streamercard-43">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle44">Channel 44</h4> <h4 class="card-title" id="streamertitle44">Channel 44</h4>
<div class="row"> <div class="row">
@@ -881,7 +882,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-44"> <div class="card card-channel" id="streamercard-44">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle-44">Channel 45</h4> <h4 class="card-title" id="streamertitle-44">Channel 45</h4>
<div class="row"> <div class="row">
@@ -900,7 +901,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-45"> <div class="card card-channel" id="streamercard-45">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle46">Channel 46</h4> <h4 class="card-title" id="streamertitle46">Channel 46</h4>
<div class="row"> <div class="row">
@@ -919,7 +920,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-46"> <div class="card card-channel" id="streamercard-46">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle47">Channel 47</h4> <h4 class="card-title" id="streamertitle47">Channel 47</h4>
<div class="row"> <div class="row">
@@ -938,7 +939,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-47"> <div class="card card-channel" id="streamercard-47">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle48">Channel 48</h4> <h4 class="card-title" id="streamertitle48">Channel 48</h4>
<div class="row"> <div class="row">
@@ -959,7 +960,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-48"> <div class="card card-channel" id="streamercard-48">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle49">Channel 49</h4> <h4 class="card-title" id="streamertitle49">Channel 49</h4>
<div class="row"> <div class="row">
@@ -978,7 +979,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-49"> <div class="card card-channel" id="streamercard-49">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle50">Channel 50</h4> <h4 class="card-title" id="streamertitle50">Channel 50</h4>
<div class="row"> <div class="row">
@@ -997,7 +998,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-50"> <div class="card card-channel" id="streamercard-50">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle51">Channel 51</h4> <h4 class="card-title" id="streamertitle51">Channel 51</h4>
<div class="row"> <div class="row">
@@ -1016,7 +1017,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-51"> <div class="card card-channel" id="streamercard-51">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle52">Channel 52</h4> <h4 class="card-title" id="streamertitle52">Channel 52</h4>
<div class="row"> <div class="row">
@@ -1037,7 +1038,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-52"> <div class="card card-channel" id="streamercard-52">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle53">Channel 53</h4> <h4 class="card-title" id="streamertitle53">Channel 53</h4>
<div class="row"> <div class="row">
@@ -1056,7 +1057,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-53"> <div class="card card-channel" id="streamercard-53">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle54">Channel 54</h4> <h4 class="card-title" id="streamertitle54">Channel 54</h4>
<div class="row"> <div class="row">
@@ -1075,7 +1076,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-54"> <div class="card card-channel" id="streamercard-54">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle55">Channel 55</h4> <h4 class="card-title" id="streamertitle55">Channel 55</h4>
<div class="row"> <div class="row">
@@ -1094,7 +1095,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-55"> <div class="card card-channel" id="streamercard-55">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle56">Channel 56</h4> <h4 class="card-title" id="streamertitle56">Channel 56</h4>
<div class="row"> <div class="row">
@@ -1115,7 +1116,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-56"> <div class="card card-channel" id="streamercard-56">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle57">Channel 57</h4> <h4 class="card-title" id="streamertitle57">Channel 57</h4>
<div class="row"> <div class="row">
@@ -1134,7 +1135,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-57"> <div class="card card-channel" id="streamercard-57">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle58">Channel 58</h4> <h4 class="card-title" id="streamertitle58">Channel 58</h4>
<div class="row"> <div class="row">
@@ -1153,7 +1154,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-58"> <div class="card card-channel" id="streamercard-58">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle59">Channel 59</h4> <h4 class="card-title" id="streamertitle59">Channel 59</h4>
<div class="row"> <div class="row">
@@ -1172,7 +1173,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-59"> <div class="card card-channel" id="streamercard-59">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle60">Channel 60</h4> <h4 class="card-title" id="streamertitle60">Channel 60</h4>
<div class="row"> <div class="row">
@@ -1193,7 +1194,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-60"> <div class="card card-channel" id="streamercard-60">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle61">Channel 61</h4> <h4 class="card-title" id="streamertitle61">Channel 61</h4>
<div class="row"> <div class="row">
@@ -1212,7 +1213,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-61"> <div class="card card-channel" id="streamercard-61">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle62">Channel 62</h4> <h4 class="card-title" id="streamertitle62">Channel 62</h4>
<div class="row"> <div class="row">
@@ -1231,7 +1232,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-62"> <div class="card card-channel" id="streamercard-62">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle63">Channel 63</h4> <h4 class="card-title" id="streamertitle63">Channel 63</h4>
<div class="row"> <div class="row">
@@ -1250,7 +1251,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card"> <div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3 col-xxl-3 pad-card">
<div class="card" id="streamercard-63"> <div class="card card-channel" id="streamercard-63">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" id="streamertitle64">Channel 64</h4> <h4 class="card-title" id="streamertitle64">Channel 64</h4>
<div class="row"> <div class="row">
@@ -1262,7 +1263,7 @@
</div> </div>
</div> </div>
<p class="card-text" id="streamerstatus64">Status : Idle</p> <p class="card-text" id="streamerstatus64">Status : Idle</p>
<div class="progress" id="streamervu-63"> <div class="progress" id="streamervu64">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div> <div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
</div> </div>
</div> </div>
@@ -1272,17 +1273,21 @@
</div> </div>
</div> </div>
</div> </div>
<div class="accordion-item pad-accordion"> <div class="accordion-item pad-accordion pad-2">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed bg-heading2" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="false" aria-controls="accordion-1 .item-2">Paging Queue</button></h2> <h2 class="accordion-header" role="tab"><button class="accordion-button collapsed bg-heading2" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="false" aria-controls="accordion-1 .item-2">Paging Queue</button></h2>
<div class="accordion-collapse collapse item-2" role="tabpanel" data-bs-parent="#accordion-1"> <div class="accordion-collapse collapse item-2 pad-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body"> <div class="accordion-body">
<div class="row"> <div class="row">
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Index</th> <th class="class05">Index</th>
<th>Description</th> <th class="class10">Date Time</th>
<th class="class15">Source</th>
<th class="class10">Type</th>
<th class="class30">Message</th>
<th class="class30">Broadcast Zones</th>
</tr> </tr>
</thead> </thead>
<tbody id="pagingqueuetable"></tbody> <tbody id="pagingqueuetable"></tbody>
@@ -1291,6 +1296,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col"><button class="btn w-100 pad-button btn-round-basic color-add" id="clearpagingqueue" type="button">Clear Queue</button></div> <div class="col"><button class="btn w-100 pad-button btn-round-basic color-add" id="clearpagingqueue" type="button">Clear Queue</button></div>
<div class="col"><button class="btn w-100 pad-button btn-round-basic color-remove" id="removepagingqueue" type="button">Remove Queue</button></div>
</div> </div>
</div> </div>
</div> </div>
@@ -1304,8 +1310,12 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Index</th> <th class="class05">Index</th>
<th>Description</th> <th class="class10">Date Time</th>
<th class="class15">Source</th>
<th class="class10">Type</th>
<th class="class30">Message</th>
<th class="class30">Broadcast Zones</th>
</tr> </tr>
</thead> </thead>
<tbody id="automaticqueuetable"></tbody> <tbody id="automaticqueuetable"></tbody>
@@ -1314,12 +1324,15 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col"><button class="btn w-100 pad-button btn-round-basic color-add" id="clearautomatictable" type="button">Clear Queue</button></div> <div class="col"><button class="btn w-100 pad-button btn-round-basic color-add" id="clearautomatictable" type="button">Clear Queue</button></div>
<div class="col"><button class="btn w-100 pad-button btn-round-basic color-remove" id="removeautomatictable" type="button">Remove Queue</button></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/overview.js"></script>
</body> </body>
</html> </html>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -16,7 +17,70 @@
<h2 style="text-align: center;">Setting</h2> <h2 style="text-align: center;">Setting</h2>
</div> </div>
</div> </div>
<div class="row">
<div class="col">
<div class="card card-setting">
<div class="card-body">
<h4 class="card-title"><strong>Upload Soundbank</strong></h4>
<hr>
<div class="row">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">Path</label></div>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"><input class="w-100 form-control" type="text" id="setting_path"></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-6 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="save_directory" type="button">Save Directory</button></div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">Category</label></div>
<div class="col-6 col-sm-10 col-md-2 col-lg-2 col-xl-2"><select id="setting_category" class="input-add form-select"></select></div>
<div class="col-6 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">Language</label></div>
<div class="col-6 col-sm-10 col-md-2 col-lg-2 col-xl-2"><select id="setting_language" class="input-add form-select"></select></div>
<div class="col-6 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">Voice</label></div>
<div class="col-6 col-sm-10 col-md-2 col-lg-2 col-xl-2"><select id="setting_voice" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
<div class="bg-white w-100" id="drop-area" multiple=""><input type="file" id="file-input"><label class="form-label d">Drop files here or click to select</label></div>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
<div class="row"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row py-5">
<div class="col">
<div class="card card-setting">
<div class="card-body pad-accordion">
<h4 class="card-title"><strong>FIS CODE</strong></h4>
<hr>
<div class="row">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">GOP</label></div>
<div class="col-10 col-sm-10 col-md-10 col-lg-10 col-xl-10"><select id="input_GOP" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">GBP</label></div>
<div class="col-10 col-sm-10 col-md-10 col-lg-10 col-xl-10"><select id="input_GBP" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">GFC</label></div>
<div class="col-10 col-sm-10 col-md-10 col-lg-10 col-xl-10"><select id="input_GFC" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label">FLD</label></div>
<div class="col-10 col-sm-10 col-md-10 col-lg-10 col-xl-10"><select id="input_FLD" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2"><label class="col-form-label"></label></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-6 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" type="button">Save</button></div>
</div>
</div>
</div>
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/dragdrop.js"></script>
</body> </body>
</html> </html>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -41,13 +42,13 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th>Description</th> <th class="class20">Description</th>
<th class="col-sm-1">TAG</th> <th class="class10">TAG</th>
<th class="col-sm-1">Category</th> <th class="class15">Category</th>
<th class="col-sm-1">Language</th> <th class="class15">Language</th>
<th class="col-sm-1">Type</th> <th class="class10">Type</th>
<th class="col-sm-3">Filename</th> <th class="class25">Filename</th>
</tr> </tr>
</thead> </thead>
<tbody id="soundbanktablebody"></tbody> <tbody id="soundbanktablebody"></tbody>
@@ -57,10 +58,10 @@
<div class="modal fade border-0" role="dialog" tabindex="-1" id="soundbankmodal"> <div class="modal fade border-0" role="dialog" tabindex="-1" id="soundbankmodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header show"> <div class="modal-header show bg-header-modal">
<h4 class="modal-title align-content-center">Add / Edit Soundbank</h4> <h4 class="modal-title align-content-center">Add / Edit Soundbank</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p> <p class="text-add">Index</p>
@@ -97,11 +98,11 @@
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="modalvoicetype" class="input-add form-select"></select></div> <div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="modalvoicetype" class="input-add form-select"></select></div>
</div> </div>
<div class="row" style="height: 100px;"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Path</p> <p class="text-add">Path</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select class="w-100 js-example-basic-single input-add form-select" id="modalpath" name="modalpath"></select></div> <div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="modalpath" class="input-add form-select cw-100" name="modalpath" data-control="select2"></select></div>
</div> </div>
</div> </div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundbankclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundbanksave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundbankclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundbanksave" type="button">Save</button></div>
@@ -109,6 +110,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/soundbank.js"></script> <script src="assets/js/soundbank.js"></script>
</body> </body>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -29,6 +30,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
</body> </body>
</html> </html>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -41,14 +42,15 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th class="col-sm-2">Description</th> <th class="class15">Description</th>
<th class="col-sm-1">Day</th> <th class="class15">Day</th>
<th class="col-sm-1">Time</th> <th class="class10">Time</th>
<th class="col-sm-2">Sound Path</th> <th class="class15">Sound Path</th>
<th class="col-sm-1">Repeat</th> <th class="class10">Repeat</th>
<th class="col-sm-1">Enable</th> <th class="class05">Enable</th>
<th>Broadcast Zones</th> <th class="class15">Broadcast Zones</th>
<th class="class10">Language</th>
</tr> </tr>
</thead> </thead>
<tbody id="schedulebanktablebody"></tbody> <tbody id="schedulebanktablebody"></tbody>
@@ -58,10 +60,10 @@
<div class="modal fade" role="dialog" tabindex="-1" id="schedulemodal"> <div class="modal fade" role="dialog" tabindex="-1" id="schedulemodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Schedule</h4> <h4 class="modal-title">Add / Edit Schedule</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p> <p class="text-add">Index</p>
@@ -86,37 +88,13 @@
</div> </div>
<div class="row pad-day"> <div class="row pad-day">
<div class="col"> <div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulesunday" name="dayselection" value="Sunday"><label class="form-check-label" for="formCheck-8">Sunday</label></div> <div class="form-check"><input class="form-check-input" type="radio" id="scheduleweekly"><label class="form-check-label" for="formCheck-1">Weekly</label></div><select class="w-100 input-add form-select" id="weeklyselect">
</div> <optgroup label="This is a group">
</div> <option value="12" selected="">This is item 1</option>
<div class="row pad-day"> <option value="13">This is item 2</option>
<div class="col"> <option value="14">This is item 3</option>
<div class="form-check"><input class="form-check-input" type="radio" id="schedulemonday" name="dayselection" value="Monday"><label class="form-check-label" for="formCheck-7">Monday</label></div> </optgroup>
</div> </select>
</div>
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="scheduletuesday" name="dayselection" value="Tuesday"><label class="form-check-label" for="formCheck-6">Tuesday</label></div>
</div>
</div>
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulewednesday" name="dayselection" value="Wednesday"><label class="form-check-label" for="formCheck-5">Wednesday</label></div>
</div>
</div>
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulethursday" name="dayselection" value="Thursday"><label class="form-check-label" for="formCheck-4">Thursday</label></div>
</div>
</div>
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulefriday" name="dayselection" value="Friday"><label class="form-check-label" for="formCheck-3">Friday</label></div>
</div>
</div>
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulesaturday" name="dayselection" value="Saturday"><label class="form-check-label" for="formCheck-2">Saturday</label></div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@@ -133,12 +111,12 @@
</div> </div>
<div class="col"> <div class="col">
<div class="row w-100 h-100"> <div class="row w-100 h-100">
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3"><input type="number" id="schedulehour" class="input-add form-control class100" value="0" min="0" max="23" step="1"></div> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"><input type="number" id="schedulehour" class="input-add form-control class100" value="0" min="0" max="23" step="1"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-3 col-xl-3"> <div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<p class="pad-time">(H)</p> <p class="pad-time">(H)</p>
</div> </div>
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3"><input class="w-100 input-add form-control" type="number" id="scheduleminute" value="0" min="0" max="59" step="1"></div> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"><input class="w-100 input-add form-control" type="number" id="scheduleminute" value="0" min="0" max="59" step="1"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-3 col-xl-3"> <div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<p class="pad-time">(M)</p> <p class="pad-time">(M)</p>
</div> </div>
</div> </div>
@@ -146,9 +124,25 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Path</p> <p class="text-add">Message</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="schedulemessage" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</div>
<div class="col">
<div class="row pad-day">
<div class="col"><select class="w-100 input-add form-select" id="languageselect">
<optgroup label="This is a group">
<option value="12" selected="">This is item 1</option>
<option value="13">This is item 2</option>
<option value="14">This is item 3</option>
</optgroup>
</select></div>
</div>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="schedulesoundpath" class="input-add form-control"></select></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
@@ -166,7 +160,13 @@
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Broadcast Zones</p> <p class="text-add">Broadcast Zones</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8 border" id="schedulezones"></div> <div class="col-8 col-sm-8 col-md-8 col-lg-9 col-xl-8 border"><select class="w-100 input-add form-select" id="schedulezones">
<optgroup label="This is a group">
<option value="12" selected="">This is item 1</option>
<option value="13">This is item 2</option>
<option value="14">This is item 3</option>
</optgroup>
</select></div>
</div> </div>
</div> </div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="scheduleclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic class25 color-add" id="schedulesave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="scheduleclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic class25 color-add" id="schedulesave" type="button">Save</button></div>
@@ -174,6 +174,7 @@
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/schedulebank.js"></script> <script src="assets/js/schedulebank.js"></script>
</body> </body>

View File

@@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title> <title>AAS_NewGen_17OKT25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css"> <link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css"> <link rel="stylesheet" href="assets/css/styles.css">
</head> </head>
@@ -41,12 +42,13 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1">No</th> <th class="class05">No</th>
<th class="col-sm-1">Username</th> <th class="class10">Username</th>
<th class="col-sm-1">Location</th> <th class="class15">Location</th>
<th class="col">Soundbank</th> <th class="class15">Airline</th>
<th class="col-sm-2">Messagebank</th> <th class="class15">City</th>
<th class="col-sm-2">Broadcast Zones</th> <th class="class20">Messagebank</th>
<th class="class20">Broadcast Zones</th>
</tr> </tr>
</thead> </thead>
<tbody id="usertablebody"></tbody> <tbody id="usertablebody"></tbody>
@@ -56,10 +58,10 @@
<div class="modal fade border-0" role="dialog" tabindex="-1" id="addmodal"> <div class="modal fade border-0" role="dialog" tabindex="-1" id="addmodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header show"> <div class="modal-header text-start show bg-header-modal">
<h4 class="modal-title align-content-center">Add / Edit User</h4> <h4 class="modal-title text-center align-content-center">Add / Edit User</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p> <p class="text-add">Index</p>
@@ -86,21 +88,53 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Bank</p> <p class="text-add">Location</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modallocation" class="input-add form-control"></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Airline Tags</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8">
<div class="row">
<div class="col-8"><input type="text" id="modalairlinetags" class="form-control input-add"></div>
<div class="col-4"><button class="btn w-100 btn-select btn-round-basic color-import" id="btnShowSoundbankModal" type="button">Select</button></div>
</div>
</div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">City Tags</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8">
<div class="row">
<div class="col-8"><input type="text" id="modalcitytags" class="form-control input-add"></div>
<div class="col-4"></div>
</div>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalsoundbank" class="input-add"><button class="btn btn-primary" id="btnShowSoundbankModal" type="button">Select</button></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Message Bank</p> <p class="text-add">Message Bank</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalmessagebank" class="input-add"><button class="btn btn-primary" id="btnShowMessagebankModal" type="button">Select</button></div> <div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8">
<div class="row">
<div class="col-8"><input type="text" id="modalmessagebank" class="form-control input-add"></div>
<div class="col-4"><button class="btn w-100 btn-round-basic color-import" id="btnShowMessagebankModal" type="button">Select</button></div>
</div>
</div>
</div> </div>
<div class="row" style="height: 100px;"> <div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"> <div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Broadcast Zones</p> <p class="text-add">Broadcast Zones</p>
</div> </div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalbroadcastzones" class="input-add" name="modalpath"><button class="btn btn-primary" id="btnShowBroaadcastZoneModal" type="button">Select</button></div> <div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8">
<div class="row">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalbroadcastzones" class="form-control input-add" name="modalpath"></div>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4"><button class="btn w-100 btn-round-basic color-import" id="btnShowBroaadcastZoneModal" type="button">Select</button></div>
</div>
</div>
</div> </div>
</div> </div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="usermanagementclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="usermanagementsave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="usermanagementclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="usermanagementsave" type="button">Save</button></div>
@@ -110,43 +144,61 @@
<div class="modal fade" role="dialog" tabindex="-1" id="soundbankmodal"> <div class="modal fade" role="dialog" tabindex="-1" id="soundbankmodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header form-control input-add">
<h4 class="modal-title">Sound Bank Selection</h4> <h4 class="modal-title">Sound Bank Selection</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<p>(mon, ganti list checkbox, dengan id=soundbankselection</p> <div class="accordion" role="tablist" id="accordion-1">
<div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button bg-heading1" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="true" aria-controls="accordion-1 .item-1">Airline</button></h2>
<div class="accordion-collapse collapse show item-1" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<ul id="airlinelist"></ul>
</div>
</div>
</div>
<div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed bg-heading2" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="false" aria-controls="accordion-1 .item-2">City</button></h2>
<div class="accordion-collapse collapse item-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<ul id="citylist"></ul>
</div>
</div>
</div>
</div>
</div> </div>
<div class="modal-footer"><button class="btn btn-light" id="soundbankselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="soundbankselectionsave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundbankselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundbankselectionsave" type="button">Save</button></div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal"> <div class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header form-control input-add">
<h4 class="modal-title">Broadcast Zones Selection</h4> <h4 class="modal-title">Broadcast Zones Selection</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<p>(mon, ganti list checkbox, dengan id=broadcastzoneselection</p> <ul id="broadcastzonelist"></ul>
</div> </div>
<div class="modal-footer"><button class="btn btn-light" id="broadcastzoneselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="broadcastzoneselectionsave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="broadcastzoneselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="broadcastzoneselectionsave" type="button">Save</button></div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal"> <div class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header form-control input-add">
<h4 class="modal-title">Message Bank Selection</h4> <h4 class="modal-title">Message Bank Selection</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body bg-modal-body">
<p>(mon, ganti list checkbox, dengan id=messagebankselection</p> <ul id="messagebanklist"></ul>
</div> </div>
<div class="modal-footer"><button class="btn btn-light" id="messagebankselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="messagebankselectionsave" type="button">Save</button></div> <div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="messagebankselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="messagebankselectionsave" type="button">Save</button></div>
</div> </div>
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/usermanagement.js"></script> <script src="assets/js/usermanagement.js"></script>
</body> </body>

View File

@@ -1,9 +1,13 @@
import audio.AudioPlayer import audio.AudioPlayer
import audio.ContentCache
import audio.TCPReceiver
import audio.UDPReceiver import audio.UDPReceiver
import barix.BarixConnection import barix.BarixConnection
import barix.TCP_Barix_Command_Server import barix.TCP_Barix_Command_Server
import codes.Somecodes
import com.sun.jna.Platform import com.sun.jna.Platform
import commandServer.TCP_Android_Command_Server import commandServer.TCP_Android_Command_Server
import content.Category
import content.Language import content.Language
import content.VoiceType import content.VoiceType
import database.Log import database.Log
@@ -14,15 +18,20 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tinylog.Logger import org.tinylog.Logger
import org.tinylog.provider.ProviderRegistry
import oshi.util.GlobalConfig import oshi.util.GlobalConfig
import web.WebApp import web.WebApp
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.concurrent.fixedRateTimer import kotlin.concurrent.fixedRateTimer
import kotlin.io.path.absolutePathString
lateinit var db: MariaDB lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap() val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
lateinit var udpreceiver: UDPReceiver lateinit var udpreceiver: UDPReceiver
const val version = "0.0.2 (23/09/2025)" lateinit var tcpreceiver: TCPReceiver
const val version = "0.0.8 (16/10/2025)"
// AAS 64 channels // AAS 64 channels
const val max_channel = 64 const val max_channel = 64
@@ -39,6 +48,52 @@ val urutan_bahasa = listOf(
Language.ARABIC.name Language.ARABIC.name
) )
val contentCache = ContentCache()
/**
* Create necessary folders if not exist
*/
fun folder_preparation(){
// sementara diset begini, nanti pake config file
Somecodes.Soundbank_directory = Paths.get("c:\\soundbank")
Files.createDirectories(Somecodes.SoundbankResult_directory)
Files.createDirectories(Somecodes.PagingResult_directory)
Files.createDirectories(Somecodes.Soundbank_directory)
Language.entries.forEach { language ->
VoiceType.entries.forEach { voice ->
Category.entries.forEach { category ->
Files.createDirectories(Somecodes.SoundbankDirectory(language, voice, category) )
}
}
}
}
/**
* Extract necessary wav files from classpath to soundbank directory
* and Load them
*/
fun files_preparation(){
val list = listOf("chimeup.wav", "chimedown.wav", "silence1s.wav", "silencehalf.wav")
list.forEach {
Somecodes.ExtractFilesFromClassPath("/$it", Somecodes.Soundbank_directory)
val pp = Somecodes.Soundbank_directory.resolve(it)
if (Files.isRegularFile(pp)){
val afi = audioPlayer.LoadAudioFile(pp.absolutePathString())
if (afi.isValid()){
Logger.info { "Common audio $it loaded from ${pp.toAbsolutePath()}" }
val key = it.substring(0, it.length - 4) // buang .wav
contentCache.addAudioFile(key, afi)
} else {
Logger.error { "Failed to load common audio $it from ${pp.toAbsolutePath()}" }
}
} else {
Logger.error { "Common audio $it not found at ${pp.toAbsolutePath()}" }
}
}
}
// Application start here // Application start here
fun main() { fun main() {
if (Platform.isWindows()) { if (Platform.isWindows()) {
@@ -46,8 +101,14 @@ fun main() {
GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true) GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true)
} }
Logger.info { "Starting AAS New Generation version $version" } Logger.info { "Starting AAS New Generation version $version" }
folder_preparation()
audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate
audioPlayer.InitAudio(1) audioPlayer.InitAudio(1)
files_preparation()
db = MariaDB() db = MariaDB()
val subcode01 = MainExtension01() val subcode01 = MainExtension01()
@@ -58,8 +119,12 @@ fun main() {
delay(1000) delay(1000)
// prioritas 1 , habisin queue paging // prioritas 1 , habisin queue paging
subcode01.Read_Queue_Paging() subcode01.Read_Queue_Paging()
// prioritas 2, habisin queue table // prioritas 2, habisin queue shalat
subcode01.Read_Queue_Table() subcode01.Read_Queue_Shalat()
// prioritas 3, habisin queue timer
subcode01.Read_Queue_Timer()
// prioritas 4, habisin queue soundbank
subcode01.Read_Queue_Soundbank()
} }
} }
// Coroutine untuk cek Schedulebank tiap menit saat detik 00 // Coroutine untuk cek Schedulebank tiap menit saat detik 00
@@ -85,6 +150,15 @@ fun main() {
Logger.error { "Failed to start UDP Receiver on port 5002" } Logger.error { "Failed to start UDP Receiver on port 5002" }
} }
tcpreceiver = TCPReceiver()
if (tcpreceiver.Start()) {
Logger.info { "TCP Receiver started on port 5002" }
} else {
Logger.error { "Failed to start TCP Receiver on port 5002" }
}
val androidserver = TCP_Android_Command_Server() val androidserver = TCP_Android_Command_Server()
androidserver.StartTcpServer(5003){ androidserver.StartTcpServer(5003){
Logger.info { it } Logger.info { it }
@@ -93,29 +167,49 @@ fun main() {
val barixserver = TCP_Barix_Command_Server() val barixserver = TCP_Barix_Command_Server()
barixserver.StartTcpServer { cmd -> barixserver.StartTcpServer { cmd ->
//Logger.info { cmd } val _tcp = barixserver.getSocket(cmd.ipaddress)
val _streamer = StreamerOutputs[cmd.ipaddress] val _streamer = StreamerOutputs[cmd.ipaddress]
val _sc = db.soundchannelDB.List.find { it.ip == cmd.ipaddress } val _sc = db.soundchannelDB.List.find { it.ip == cmd.ipaddress }
if (_streamer == null) { if (_streamer == null) {
// belum create BarixConnection untuk ipaddress ini // belum create BarixConnection untuk ipaddress ini
Logger.info { "New Streamer Output connection from ${cmd.ipaddress}" } //Logger.info { "New Streamer Output connection from ${cmd.ipaddress}" }
if (_sc != null) { if (_sc != null) {
val _bc = BarixConnection(_sc.index, _sc.channel, cmd.ipaddress) val _bc = BarixConnection(_sc.index, _sc.channel, cmd.ipaddress)
_bc.vu = cmd.vu // cmd.vu 0 - 32767, kita convert ke 0 - 100
_bc.vu = ((1.0 * cmd.vu / 32767.0)* 100.0).toInt()
_bc.bufferRemain = cmd.buffremain _bc.bufferRemain = cmd.buffremain
_bc.statusData = cmd.statusdata _bc.statusData = cmd.statusdata
_bc.commandsocket = _tcp
StreamerOutputs[cmd.ipaddress] = _bc StreamerOutputs[cmd.ipaddress] = _bc
Logger.info { "Created new Streamer Output for channel ${_sc.channel} with IP ${cmd.ipaddress}" } Logger.info { "Created new Streamer Output for channel ${_sc.channel} with IP ${cmd.ipaddress}" }
} else Logger.warn { "soundChannelDB doesn't have soundchannel with IP ${cmd.ipaddress}" } }
} else { } else {
// sudah ada, update data // sudah ada, update data
if (_sc != null && _sc.channel != _streamer.channel) { if (_sc != null && _sc.channel != _streamer.channel) {
_streamer.channel = _sc.channel _streamer.channel = _sc.channel
} }
_streamer.vu = cmd.vu // cmd.vu 0 - 32767, kita convert ke 0 - 100
_streamer.vu = ((1.0 * cmd.vu / 32767.0)* 100.0).toInt()
_streamer.bufferRemain = cmd.buffremain _streamer.bufferRemain = cmd.buffremain
_streamer.statusData = cmd.statusdata _streamer.statusData = cmd.statusdata
// cek apakah koneksi TCP nya ganti
if (_streamer.commandsocket == null) {
_streamer.commandsocket = _tcp
} else {
if (_streamer.commandsocket != _tcp) {
// ganti koneksi
try {
_streamer.commandsocket?.close()
} catch (ex: Exception) {
Logger.error(ex) { "Error closing previous TCP command socket for ${cmd.ipaddress}" }
}
_streamer.commandsocket = _tcp
}
}
} }
} }
@@ -127,17 +221,22 @@ fun main() {
} }
} }
db.Add_Log("AAS"," Application started")
// shutdown hook // shutdown hook
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(Thread {
db.Add_Log("AAS"," Application stopping")
Logger.info { "Shutdown hook called, stopping services..." } Logger.info { "Shutdown hook called, stopping services..." }
barixserver.StopTcpCommand() barixserver.StopTcpCommand()
androidserver.StopTcpCommand() androidserver.StopTcpCommand()
onlinechecker.cancel() onlinechecker.cancel()
web.Stop() web.Stop()
udpreceiver.Stop() udpreceiver.Stop()
tcpreceiver.Stop()
audioPlayer.Close() audioPlayer.Close()
db.close() db.close()
Logger.info { "All services stopped, exiting application." } Logger.info { "All services stopped, exiting application." }
ProviderRegistry.getLoggingProvider().shutdown()
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,19 @@
package audio package audio
import audio.Bass.BASS_ACTIVE_PLAYING
import audio.Bass.BASS_DEVICE_ENABLED import audio.Bass.BASS_DEVICE_ENABLED
import audio.Bass.BASS_DEVICE_INIT import audio.Bass.BASS_DEVICE_INIT
import audio.Bass.BASS_POS_BYTE import audio.Bass.BASS_POS_BYTE
import audio.Bass.BASS_STREAM_DECODE import audio.Bass.BASS_STREAM_DECODE
import audio.Bass.BASS_SAMPLE_MONO import audio.Bass.BASS_SAMPLE_MONO
import audio.Bass.STREAMPROC_PUSH
import audio.BassEnc.BASS_ENCODE_PCM import audio.BassEnc.BASS_ENCODE_PCM
import codes.Result_Boolean_String
import codes.Somecodes.Companion.ValidFile import codes.Somecodes.Companion.ValidFile
import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.ValidString
import com.sun.jna.Memory import com.sun.jna.Memory
import kotlinx.coroutines.CoroutineName import com.sun.jna.Pointer
import kotlinx.coroutines.Dispatchers import contentCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.tinylog.Logger import org.tinylog.Logger
import java.util.function.BiConsumer
@Suppress("unused") @Suppress("unused")
class AudioPlayer (var samplingrate: Int) { class AudioPlayer (var samplingrate: Int) {
@@ -126,81 +123,183 @@ class AudioPlayer (var samplingrate: Int) {
return result return result
} }
fun WavWriter(data: ByteArray, target: String, callback: BiConsumer<Boolean, String>) { /**
val source = AudioFileInfo() * Writes the audio data from a byte array to a WAV file.
source.bytes = data * @param data The byte array containing the audio data.
source.fileName = "In-Memory Data" * @param target The target file name for the WAV file.
val sources = listOf(source) * @param withChime If true, adds a chime sound at the beginning and end of the audio.
WavWriter(sources, target, callback) * @return A Result_Boolean_String indicating success or failure and a message.
*/
fun WavWriter(data: ByteArray, target: String, withChime: Boolean = true) : Result_Boolean_String {
bass.BASS_SetDevice(0) // Set to No Sound device for writing
val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, Pointer(-1), null)
if (streamhandle!=0){
val encodehandle = bassenc.BASS_Encode_Start(streamhandle, target, BASS_ENCODE_PCM, null, null)
if (encodehandle!=0){
fun pushData(data: ByteArray): Boolean {
val mem = Memory(data.size.toLong())
mem.write(0, data, 0, data.size)
val pushresult = bass.BASS_StreamPutData(streamhandle, mem, data.size)
if (pushresult==-1){
Logger.error { "BASS_StreamPutData failed: ${bass.BASS_ErrorGetCode()}" }
}
return pushresult != -1
}
var all_success = true
if (withChime){
val chup = contentCache.getAudioFile("chimeup")
if (chup!=null && chup.isValid()){
if (!pushData(chup.bytes)){
all_success = false
Logger.error { "Failed to push Chime Up" }
}
} else Logger.error { "withChime=true, but Chime Up not available" }
}
if (!pushData(data)){
all_success = false
Logger.error { "Failed to push Data ByteArray" }
}
if (withChime){
val chdn = contentCache.getAudioFile("chimedown")
if (chdn!=null && chdn.isValid()){
if (!pushData(chdn.bytes)){
all_success = false
Logger.error { "Failed to push Chime Down" }
}
} else Logger.error { "withChime=true, but Chime Down not available" }
}
val readsize: Long = 1024 * 1024 // read 1 MB at a time
var totalread: Long = 0
do{
val p = Memory(readsize)
val read = bass.BASS_ChannelGetData(streamhandle, p, 4096)
if (read > 0) {
totalread += read
}
} while (read > 0)
bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_StreamFree(streamhandle)
return if (all_success){
Result_Boolean_String(true, "WAV file written successfully: $target")
} else {
Result_Boolean_String(false, "Failed to write some data to WAV file: $target")
}
} else {
bass.BASS_StreamFree(streamhandle)
return Result_Boolean_String(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
}
} else {
return Result_Boolean_String(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
}
} }
/** /**
* Writes the audio data from the sources to a WAV file. * Writes the audio data from the sources to a WAV file.
* @param sources List of AudioFileInfo objects containing the audio data to write. * @param sources List of AudioFileInfo objects containing the audio data to write.
* @param target The target file name for the WAV file. * @param target The target file name for the WAV file.
* @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message. * @param withChime If true, adds a chime sound at the beginning and end of the audio.
* @return A Result_Boolean_String indicating success or failure and a message.
*/ */
fun WavWriter(sources: List<AudioFileInfo>, target: String, callback: BiConsumer<Boolean, String>) { fun WavWriter(sources: List<AudioFileInfo>, target: String, withChime: Boolean = true) : Result_Boolean_String {
if (sources.isEmpty() || !ValidFile(target)) { if (sources.isEmpty()) {
callback.accept(false, " Invalid sources") return Result_Boolean_String(false,"Invalid Source")
return
} }
if (!ValidString(target)) { if (!ValidString(target)) {
callback.accept(false, " Invalid target file name") return Result_Boolean_String(false, " Invalid target file name")
return
} }
val job = CoroutineScope(Dispatchers.Default) bass.BASS_SetDevice(0) // Set to No Sound device for writing
job.launch(CoroutineName("WavWriter $target")) { val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, Pointer(-1), null)
bass.BASS_SetDevice(0) // Set to No Sound device for writing if (streamhandle==0){
val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, STREAMPROC_PUSH, null) return Result_Boolean_String(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
if (streamhandle==0){ }
callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}") val encodehandle = bassenc.BASS_Encode_Start(streamhandle, target, BASS_ENCODE_PCM, null, null)
return@launch if (encodehandle==0){
} bass.BASS_StreamFree(streamhandle)
val encodehandle = bassenc.BASS_Encode_Start(streamhandle, target, BASS_ENCODE_PCM, null, null) return Result_Boolean_String(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
if (encodehandle==0){ }
bass.BASS_StreamFree(streamhandle)
callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
return@launch
}
val playresult = bass.BASS_ChannelPlay(streamhandle,false)
if (!playresult) {
bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_StreamFree(streamhandle)
callback.accept(false, "BASS_ChannelPlay failed: ${bass.BASS_ErrorGetCode()}")
return@launch
}
var allsuccess = true fun pushData(data: ByteArray): Boolean {
sources.forEach { source -> val mem = Memory(data.size.toLong())
if (source.isValid()) { mem.write(0, data, 0, data.size)
// write the bytes to the stream val pushresult = bass.BASS_StreamPutData(streamhandle, mem, data.size)
val mem = Memory(source.bytes.size.toLong()) if (pushresult==-1){
mem.write(0, source.bytes, 0, source.bytes.size) Logger.error { "BASS_StreamPutData failed: ${bass.BASS_ErrorGetCode()}" }
val pushresult = bass.BASS_StreamPutData(streamhandle, mem, source.bytes.size) }
if (pushresult == -1) { return pushresult != -1
Logger.error { "Failed to write data from ${source.fileName} to stream: ${bass.BASS_ErrorGetCode()}" } }
allsuccess = false
}
var allsuccess = true
if (withChime){
val chup = contentCache.getAudioFile("chimeup")
if (chup!=null && chup.isValid()){
if (!pushData(chup.bytes)){
allsuccess = false
Logger.error { "Failed to push Chime Up" }
}
} else Logger.error { "withChime=true, but Chime Up not available" }
}
sources.forEach { source ->
if (source.isValid()) {
// write the bytes to the stream
if (!pushData(source.bytes)){
allsuccess = false
Logger.error { "Source ${source.fileName} push failed" }
} }
}
// now we wait until the stream is finished
while(bassenc.BASS_Encode_IsActive(encodehandle) == BASS_ACTIVE_PLAYING) {
Thread.sleep(100) // Sleep for a short time to avoid busy waiting
}
// close the encoding handle
bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_ChannelFree(streamhandle)
if (allsuccess){
callback.accept(true, "WAV file written successfully: $target")
} else { } else {
callback.accept(false, "Failed to write some data to WAV file: $target") allsuccess = false
Logger.error { "Not pushing Source=${source.fileName} because invalid" }
} }
}
if (withChime){
val chdn = contentCache.getAudioFile("chimedown")
if (chdn!=null && chdn.isValid()){
if (!pushData(chdn.bytes)){
allsuccess = false
Logger.error { "Failed to push Chime Down" }
}
} else Logger.error { "withChime=true, but Chime Down not available"}
} }
val readsize: Long = 1024 * 1024 // read 1 MB at a time
var totalread: Long = 0
do{
val p = Memory(readsize)
val read = bass.BASS_ChannelGetData(streamhandle, p, 4096)
if (read > 0) {
totalread += read
}
} while (read > 0)
// close the encoding handle
bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_ChannelFree(streamhandle)
return if (allsuccess){
Result_Boolean_String(true, "WAV file written successfully: $target")
} else {
Result_Boolean_String(false, "Failed to write some data to WAV file: $target")
}

View File

@@ -716,6 +716,7 @@ public interface Bass extends Library {
boolean BASS_SampleStop(int handle); boolean BASS_SampleStop(int handle);
int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user); int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user);
int BASS_StreamCreate(int freq, int chans, int flags, Pointer proc, Pointer user); // for STREAMPROC_DUMMY
int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags); int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags);
int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags); int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags);
int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user); int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user);
@@ -785,8 +786,7 @@ public interface Bass extends Library {
boolean BASS_FXGetParameters(int handle, Object params); boolean BASS_FXGetParameters(int handle, Object params);
boolean BASS_FXSetPriority(int handle, int priority); boolean BASS_FXSetPriority(int handle, int priority);
boolean BASS_FXReset(int handle); boolean BASS_FXReset(int handle);
// gak bisa
int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user);
} }

55
src/audio/ContentCache.kt Normal file
View File

@@ -0,0 +1,55 @@
package audio
/**
* Cache for audio content to avoid reloading from disk
*/
@Suppress("unused")
class ContentCache {
private val map: MutableMap<String, AudioFileInfo> = HashMap()
/**
* Clear the cache, but keep essential sounds : chimeup, chimedown, silence1s, silencehalf
*/
fun clear(){
// dont clear chimeup, chimedown, silence1s, silencehalf
val keysToKeep = setOf("chimeup", "chimedown", "silence1s", "silencehalf")
map.keys.retainAll(keysToKeep)
}
/**
* Add an audio file to the cache
* @param key The key to identify the audio file
* @param audioFile The AudioFileInfo object
*/
fun addAudioFile(key: String, audioFile: AudioFileInfo) {
map[key] = audioFile
}
/**
* Retrieve an audio file from the cache
* @param key The key to identify the audio file
* @return The AudioFileInfo object, or null if not found
*/
fun getAudioFile(key: String): AudioFileInfo? {
return map[key]
}
/**
* Remove an audio file from the cache
* @param key The key to identify the audio file
*/
fun removeAudioFile(key: String) {
map.remove(key)
}
/**
* Check if the cache contains the specified key
* @param key The key to check in the cache
* @return True if the key exists, false otherwise
*/
fun haveKey(key: String): Boolean {
return map.containsKey(key)
}
}

104
src/audio/TCPReceiver.kt Normal file
View File

@@ -0,0 +1,104 @@
package audio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tinylog.Logger
import java.net.ServerSocket
import java.util.function.Consumer
/**
* TCPReceiver is a class that listens for TCP connections on a specified port.
* for receiving PCMFILE from Android SAMI
*/
class TCPReceiver(val portnumber: Int = 5002){
private lateinit var server: ServerSocket
private var isRunning = false
private val dataCallback = mutableMapOf<String, Consumer<ByteArray>>()
private val isfinishd = mutableMapOf<String, Boolean>()
/**
* Start listening for TCP connections on the specified port.
* @return true if successful, false otherwise
*/
fun Start() : Boolean{
try{
server = ServerSocket(portnumber)
isRunning = true
Logger.info { "Server started at port $portnumber" }
CoroutineScope(Dispatchers.IO).launch {
while(isRunning){
try {
val client = server.accept()
val clientAddress = client.inetAddress.hostAddress
CoroutineScope(Dispatchers.IO).launch {
isfinishd[clientAddress] = false
var totalbytes = 0L
try{
val din = client.getInputStream()
Logger.info{ "Start receiving PCMFILE from Android with IP=${clientAddress}" }
do {
val buffer = ByteArray(16384)
val bytesRead = din.read(buffer)
if (bytesRead>0){
val data = ByteArray(bytesRead)
System.arraycopy(buffer, 0, data, 0, bytesRead)
//println("Received $bytesRead bytes from $clientAddress")
totalbytes+=bytesRead
dataCallback[clientAddress].let {
it?.accept(data)
}
}
} while (bytesRead > 0)
} catch (e : Exception){
Logger.error { "Failed receiving data from $clientAddress, Message : ${e.message}" }
}
Logger.info { "Connection from $clientAddress ended, total bytesRead=$totalbytes" }
isfinishd[clientAddress] = true
}
} catch (e: Exception) {
Logger.error { "Failed to accept socket, Message : ${e.message}" }
}
}
}
return true
} catch (e : Exception){
Logger.error { "Failed to Start Server at port $portnumber, Message : ${e.message}" }
return false
}
}
fun RequestDataFrom(ipaddress: String, cb: Consumer<ByteArray>){
dataCallback[ipaddress] = cb
}
fun StopRequestDataFrom(ipaddress: String){
if (isfinishd[ipaddress] != null){
if (isfinishd[ipaddress]==false){
// belum selesai
//println("Waiting for receiving from $ipaddress to finish...")
runBlocking {
while (isfinishd[ipaddress] == false){
kotlinx.coroutines.delay(100)
}
}
}
//println("Removing callback for $ipaddress")
dataCallback.remove(ipaddress)
}
}
/**
* Stop listening for TCP connections and close the server socket.
*/
fun Stop(){
if (isRunning){
isRunning = false
server.close()
}
}
}

View File

@@ -6,9 +6,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.net.DatagramPacket import java.net.DatagramPacket
import java.net.DatagramSocket import java.net.DatagramSocket
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.function.Consumer import java.util.function.Consumer
@@ -18,9 +20,9 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private var _sd: Int = 0 private var _sd: Int = 0
private var _vu: Int = 0 private var _vu: Int = 0
private var _onlinecounter = 0 private var _onlinecounter = 0
private val udp = DatagramSocket(0)
private val inet = InetSocketAddress(ipaddress, port) private val inet = InetSocketAddress(ipaddress, port)
private val maxUDPsize = 1000
private var _tcp: Socket? = null
/** /**
@@ -53,6 +55,15 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
_onlinecounter = 5 _onlinecounter = 5
} }
/**
* TCP command socket for communication with this Barix device
*/
var commandsocket: Socket?
get() = _tcp
set(value){
_tcp = value
}
/** /**
* Decrement online counter, if counter reaches 0, the device is considered offline * Decrement online counter, if counter reaches 0, the device is considered offline
*/ */
@@ -93,23 +104,29 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) { fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data) DatagramSocket().use{ udp ->
while(bb.hasRemaining()){ val bb = ByteBuffer.wrap(data)
try { while(bb.hasRemaining()){
val chunk = ByteArray(if (bb.remaining() > 1400) 1400 else bb.remaining()) try {
bb.get(chunk) val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining())
udp.send(DatagramPacket(chunk, chunk.size, inet)) bb.get(chunk)
delay(5) //println("Buffer remain: $bufferRemain, sending chunk size: ${chunk.size}")
} catch (e: Exception) { while(bufferRemain<chunk.size){
cbFail.accept("SendData to $ipaddress:$port failed, message: ${e.message}") delay(10)
return@launch //println("Waiting until buffer enough..")
}
udp.send(DatagramPacket(chunk, chunk.size, inet))
delay(2)
} catch (e: Exception) {
cbFail.accept("SendData to $ipaddress:$port failed, message: ${e.message}")
return@launch
}
} }
cbOK.accept("SendData to $channel ($ipaddress:$port) succeeded, ${data.size} bytes sent")
} }
cbOK.accept("SendData to $channel ($ipaddress:$port) succeeded, ${data.size} bytes sent")
} }
} } else cbFail.accept("SendData to $ipaddress:$port failed, data is empty")
} }
@@ -138,4 +155,76 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
fun toJsonString(): String { fun toJsonString(): String {
return Somecodes.toJsonString(toJsonNode()) return Somecodes.toJsonString(toJsonNode())
} }
/**
* Activate relay on Barix device
* @param relays The relay numbers to activate (1-8)
* @return true if successful
*/
fun ActivateRelay(vararg relays: Int){
val command = StringBuilder("RELAY;")
var binary = 0
relays.forEach {
if (it in 1..8) {
binary = binary or (1 shl (it - 1))
}
}
command.append(binary.toString()).append("@")
SendCommand(command.toString())
}
fun ActivateRelay(relays: List<Int>){
val command = StringBuilder("RELAY;")
var binary = 0
relays.forEach {
if (it in 1..8) {
binary = binary or (1 shl (it - 1))
}
}
command.append(binary.toString()).append("@")
SendCommand(command.toString())
}
/**
* Deactivate relay on Barix device
*/
fun DeactivateRelay(){
SendCommand("RELAY;0@")
}
/**
* Send command to Barix device
* @param command The command to send
* @return true if successful
*/
fun SendCommand(command: String): Boolean {
try {
if (_tcp!=null){
if (!_tcp!!.isClosed){
val bb = command.toByteArray()
val size = bb.size + 4
val b4 = byteArrayOf(
(size shr 24 and 0xFF).toByte(),
(size shr 16 and 0xFF).toByte(),
(size shr 8 and 0xFF).toByte(),
(size and 0xFF).toByte()
)
val out = _tcp!!.getOutputStream()
out.write(b4)
out.write(bb)
out.flush()
Logger.info { "SendCommand to $ipaddress : $command" }
return true
}else {
Logger.error { "Socket to $ipaddress is not connected" }
}
} else {
Logger.error { "Socket to $ipaddress is null" }
}
} catch (e: Exception) {
Logger.error { "Failed to SendCommand to $ipaddress, Message : ${e.message}" }
}
return false
}
} }

View File

@@ -3,8 +3,10 @@ package barix
import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.ValidString
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.tinylog.Logger import org.tinylog.Logger
import java.io.DataInputStream
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer
import java.util.function.Consumer import java.util.function.Consumer
@Suppress("unused") @Suppress("unused")
@@ -37,13 +39,19 @@ class TCP_Barix_Command_Server {
val key : String = socket.inetAddress.hostAddress val key : String = socket.inetAddress.hostAddress
socketMap[key] = socket socketMap[key] = socket
Logger.info { "Start communicating with Streamer Output with IP : $key" } Logger.info { "Start communicating with Streamer Output with IP : $key" }
val din = socket.getInputStream() try{
while (isActive) {
if (din.available()>0){ val din = DataInputStream(socket.getInputStream())
val bb = ByteArray(din.available()) while (isActive) {
din.read(bb) val length = ByteArray(4)
din.readFully(length)
val readlength = ByteBuffer.wrap(length).getInt()
//println("Read Length : $readlength")
val bb = ByteArray(readlength)
din.readFully(bb)
// B4A format, 4 bytes di depan adalah size // B4A format, 4 bytes di depan adalah size
val str = String(bb, 4, bb.size - 4) val str = String(bb)
//println("Received from $key : $str")
if (ValidString(str)) { if (ValidString(str)) {
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$ // Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
pattern.find(str)?.let { matchResult -> pattern.find(str)?.let { matchResult ->
@@ -60,8 +68,12 @@ class TCP_Barix_Command_Server {
Logger.warn { "Invalid command format from $key : $str" } Logger.warn { "Invalid command format from $key : $str" }
} }
} }
} }
} catch (ex:Exception){
Logger.error { "Error in communication with Streamer Output with IP $key, Message : ${ex.message}" }
} }
Logger.info { "Finished communicating with Streamer Output with IP $key" } Logger.info { "Finished communicating with Streamer Output with IP $key" }
socketMap.remove(key) socketMap.remove(key)
@@ -102,4 +114,15 @@ class TCP_Barix_Command_Server {
} }
return false return false
} }
/**
* Get Socket by IP address
* @param ip The IP address of the client
* @return Socket if found, null otherwise
*/
fun getSocket(ip: String): Socket? {
return socketMap[ip]
}
} }

View File

@@ -0,0 +1,4 @@
package codes
class Result_Boolean_String(val success: Boolean, val message: String) {
}

View File

@@ -0,0 +1,3 @@
package codes
class Result_GetSoundbankFiles(val success: Boolean, val message: String , val files: List<String> = emptyList())

View File

@@ -2,14 +2,22 @@ package codes
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import content.Category
import content.Language
import content.NetworkInformation
import content.ScheduleDay import content.ScheduleDay
import content.VoiceType
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tinylog.Logger
import oshi.SystemInfo import oshi.SystemInfo
import oshi.hardware.CentralProcessor import oshi.hardware.CentralProcessor
import oshi.hardware.GlobalMemory import oshi.hardware.GlobalMemory
import oshi.hardware.NetworkIF
import oshi.hardware.Sensors
import oshi.software.os.OperatingSystem
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -22,11 +30,18 @@ import kotlin.io.path.name
class Somecodes { class Somecodes {
companion object { companion object {
val current_directory : String = System.getProperty("user.dir") val current_directory : String = System.getProperty("user.dir")
var Soundbank_directory : Path = Path.of(current_directory,"Soundbank")
val SoundbankResult_directory : Path = Path.of(current_directory,"SoundbankResult") val SoundbankResult_directory : Path = Path.of(current_directory,"SoundbankResult")
val PagingResult_directory : Path = Path.of(current_directory,"PagingResult") val PagingResult_directory : Path = Path.of(current_directory,"PagingResult")
val si = SystemInfo() val si = SystemInfo()
val processor: CentralProcessor = si.hardware.processor val processor: CentralProcessor = si.hardware.processor
val memory : GlobalMemory = si.hardware.memory val memory : GlobalMemory = si.hardware.memory
val NetworkInfoMap = mutableMapOf<String, NetworkInformation>()
val sensor : Sensors = si.hardware.sensors
val os : OperatingSystem = si.operatingSystem
val datetimeformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss") val datetimeformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
val dateformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy") val dateformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
val dateformat2: DateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy") val dateformat2: DateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy")
@@ -42,6 +57,58 @@ class Somecodes {
// regex for getting ann_id from Message, which is the number inside [] // regex for getting ann_id from Message, which is the number inside []
private val ann_id_regex = Regex("\\[(\\d+)]") private val ann_id_regex = Regex("\\[(\\d+)]")
/**
* Get the directory path for a specific language, voice type, and category.
* @param language The language.
* @param voice The voice type.
* @param category The category.
* @return The path to the directory.
*/
fun SoundbankDirectory(language: Language, voice: VoiceType, category: Category) : Path{
return Soundbank_directory.resolve(language.name).resolve(voice.name).resolve(category.name)
}
fun SoundbankDirectory(language: String, voice: String, category: String) : Path{
return SoundbankDirectory(
Language.valueOf(language),
VoiceType.valueOf(voice),
Category.valueOf(category)
)
}
fun ExtractFilesFromClassPath(resourcePath: String, outputDir: Path) {
try {
val resource = Somecodes::class.java.getResource(resourcePath)
if (resource != null) {
val uri = resource.toURI()
val path = if (uri.scheme == "jar") {
val fileSystem = java.nio.file.FileSystems.newFileSystem(uri, emptyMap<String, Any>())
fileSystem.getPath(resourcePath)
} else {
Path.of(uri)
}
Files.walk(path).use { stream ->
stream.forEach { sourcePath ->
if (Files.isRegularFile(sourcePath)) {
val fn = sourcePath.fileName
val targetPath = outputDir.resolve(fn)
if (Files.isRegularFile(targetPath) && Files.size(targetPath) > 0) {
Logger.info { "File $targetPath already exists, skipping extraction." }
return@forEach
}
Files.copy(sourcePath, targetPath)
Logger.info { "Extracted ${sourcePath.name} to $targetPath" }
return@forEach
}
}
}
} else Logger.error { "Resource $resource not found" }
} catch (e: Exception) {
Logger.error { "Exception while extracting $resourcePath, Message = ${e.message}"}
}
}
/** /**
* Check if a string is a valid number. * Check if a string is a valid number.
*/ */
@@ -110,14 +177,13 @@ class Somecodes {
/** /**
* List all audio files (.mp3 and .wav) in the specified directory and its subdirectories. * List all audio files (.mp3 and .wav) in the specified directory and its subdirectories.
* Only files larger than 1KB are included. * Only files larger than 1KB are included.
* @param dir The directory to search in (default is the current working directory). * @param p The directory Path to search in
* @return A list of absolute paths to the audio files found. * @return A list of absolute paths to the audio files found.
*/ */
fun ListAudioFiles(dir: String = current_directory) : List<String>{ fun ListAudioFiles(p: Path) : List<String>{
return try{ return try{
// find all files that ends with .mp3 or .wav // find all files that ends with .mp3 or .wav
// and find them recursively // and find them recursively
val p = Path.of(dir)
if (Files.exists(p) && Files.isDirectory(p)){ if (Files.exists(p) && Files.isDirectory(p)){
Files.walk(p) Files.walk(p)
// cari file regular saja // cari file regular saja
@@ -218,6 +284,45 @@ class Somecodes {
, (usedMemory.toDouble() / totalMemory * 100)) , (usedMemory.toDouble() / totalMemory * 100))
} }
fun GetNetworkStatus(cb : Consumer<List<NetworkInformation>>) {
val networks: List<NetworkIF> = si.hardware.networkIFs.toList()
networks.forEach { net ->
if (net.ifOperStatus==NetworkIF.IfOperStatus.UP){
if (net.iPv4addr.size>0 || net.iPv6addr.size>0){
var ni = NetworkInfoMap[net.name]
if (ni == null){
ni = NetworkInformation(net.name, net.displayName, net.macaddr)
NetworkInfoMap[net.name] = ni
}
ni.ipV4addr = net.iPv4addr.toMutableList()
ni.ipV6addr = net.iPv6addr.toMutableList()
ni.speed = net.speed
ni.packetsSent = net.packetsSent
ni.packetsRecv = net.packetsRecv
if (ni.updateStamp==0L){
ni.bytesSent = net.bytesSent
ni.bytesRecv = net.bytesRecv
ni.txSpeed = 0
ni.rxSpeed = 0
ni.updateStamp = System.currentTimeMillis()
} else {
// tx speed = (current bytesSent - previous bytesSent) / (current time - previous time) * 1000
val currentTime = System.currentTimeMillis()
ni.txSpeed = ((net.bytesSent - ni.bytesSent) * 1000 / (currentTime - ni.updateStamp))
ni.rxSpeed = ((net.bytesRecv - ni.bytesRecv) * 1000 / (currentTime - ni.updateStamp))
ni.bytesSent = net.bytesSent
ni.bytesRecv = net.bytesRecv
ni.updateStamp = currentTime
}
} else if (NetworkInfoMap.contains(net.name)) NetworkInfoMap.remove(net.name)
} else if (NetworkInfoMap.contains(net.name)) NetworkInfoMap.remove(net.name)
}
cb.accept(NetworkInfoMap.values.toList())
}
/** /**
* Check if a value is a valid non-blank string. * Check if a value is a valid non-blank string.
* @param value The value to check. * @param value The value to check.
@@ -227,6 +332,23 @@ class Somecodes {
return value is String && value.isNotBlank() return value is String && value.isNotBlank()
} }
/**
* Check if all strings in a list are valid non-blank strings.
* @param values The list of strings to check.
* @return True if all strings in the list are valid non-blank strings, false otherwise.
*/
fun ValidStrings(values: List<String>) : Boolean{
if (values.isNotEmpty()){
for (v in values){
if (!ValidString(v)){
return false
}
}
return true
}
return false
}
/** /**
* Check if a string is a valid file path and the file exists. * Check if a string is a valid file path and the file exists.
* @param value The string to check. * @param value The string to check.
@@ -374,6 +496,35 @@ class Somecodes {
sb.append(".wav") sb.append(".wav")
return sb.toString() return sb.toString()
} }
/**
* Get sensors information using OSHI library.
* @return A string representing the CPU temperature, fan speeds, and CPU voltage, or an empty string if not available.
*/
fun GetSensorsInfo() : String {
val cputemp = sensor.cpuTemperature
val cpuvolt = sensor.cpuVoltage
val fanspeed = sensor.fanSpeeds
return if (cpuvolt>0 && cputemp > 0 && fanspeed.isNotEmpty()){
String.format("CPU Temp: %.1f °C\nFan Speeds: %s RPM\nCPU Voltage: %.2f V",
sensor.cpuTemperature,
sensor.fanSpeeds.joinToString("/"),
sensor.cpuVoltage
)
} else ""
}
fun GetUptime() : String {
val value = os.systemUptime
return if (value>0){
// number of seconds since system boot
val hours = value / 3600
val minutes = (value % 3600) / 60
val seconds = value % 60
String.format("%02d:%02d:%02d", hours, minutes, seconds)
} else ""
}
} }

View File

@@ -2,7 +2,6 @@ package commandServer
import codes.Somecodes.Companion.PagingResult_directory import codes.Somecodes.Companion.PagingResult_directory
import codes.Somecodes.Companion.filenameformat import codes.Somecodes.Companion.filenameformat
import org.tinylog.Logger
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.file.Path import java.nio.file.Path
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -13,11 +12,16 @@ import java.time.LocalDateTime
* @param broadcastzones The zones to which the paging is broadcasted, is a semicolon-separated string. * @param broadcastzones The zones to which the paging is broadcasted, is a semicolon-separated string.
*/ */
class PagingJob(val fromIP: String, val broadcastzones: String) { class PagingJob(val fromIP: String, val broadcastzones: String) {
val filePath : Path = PagingResult_directory.resolve(LocalDateTime.now().format(filenameformat)+"_RAW.wav") val filePath : Path = PagingResult_directory.resolve("PAGING_"+fromIP+"_"+LocalDateTime.now().format(filenameformat)+".wav")
private val bos : ByteArrayOutputStream = ByteArrayOutputStream() private val bos : ByteArrayOutputStream = ByteArrayOutputStream()
var totalBytesReceived = 0; private set var totalBytesReceived = 0; private set
var isRunning = true; private set var isRunning = true; private set
/**
* Expected Size from PCMFILE android
*/
var expectedSize = 0
/** /**
* Adds incoming audio data to the job. * Adds incoming audio data to the job.
@@ -25,7 +29,6 @@ class PagingJob(val fromIP: String, val broadcastzones: String) {
* @param length The number of bytes to write from the data array. * @param length The number of bytes to write from the data array.
*/ */
fun addData(data: ByteArray, length: Int) { fun addData(data: ByteArray, length: Int) {
Logger.info{"PagingJob from $fromIP, zones: $broadcastzones, received $length bytes"}
bos.write(data, 0, length) bos.write(data, 0, length)
totalBytesReceived += length totalBytesReceived += length
} }

View File

@@ -3,6 +3,7 @@ package commandServer
import audioPlayer import audioPlayer
import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.datetimeformat1 import codes.Somecodes.Companion.datetimeformat1
import content.Category
import content.Language import content.Language
import database.Messagebank import database.Messagebank
import database.QueuePaging import database.QueuePaging
@@ -16,6 +17,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tinylog.Logger import org.tinylog.Logger
import tcpreceiver
import udpreceiver import udpreceiver
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
@@ -50,42 +52,45 @@ class TCP_Android_Command_Server {
while (isActive) { while (isActive) {
if (tcpserver.isClosed) break if (tcpserver.isClosed) break
try { try {
tcpserver.accept().use { socket -> val socket = tcpserver.accept()
{
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (socket != null) { if (socket != null) {
// key is IP address only // key is IP address only
val key: String = socket.inetAddress.hostAddress val key: String = socket.inetAddress.hostAddress
socketMap[key] = socket socketMap[key] = socket
Logger.info { "Start communicating with $key" } Logger.info { "Start communicating with IPMT/IPM with IP $key" }
socket.getInputStream().let { din -> val din = socket.getInputStream()
{ val dout = socket.getOutputStream()
while (isActive) { try{
if (din.available() > 0) { while (isActive) {
val bb = ByteArray(din.available()) if (din.available() > 0) {
din.read(bb) val bb = ByteArray(din.available())
// B4A format, 4 bytes di depan adalah size din.read(bb)
val str = String(bb, 4, bb.size - 4) // B4A format, 4 bytes di depan adalah size
str.split("@").map { it.trim() }.filter { ValidString(it) } val str = String(bb, 4, bb.size - 4)
.map { it.uppercase() }.forEach { //println("Received command from $key : $str")
process_command(key,it) { reply -> str.split("@").map { it.trim() }.filter { ValidString(it) }
try { .forEach {
socket.getOutputStream().write(String_to_Byte_Android(reply)) process_command(key,it) { reply ->
} catch (e: Exception) { try {
logcb.accept("Failed to send reply to $key, Message : ${e.message}") dout.write(String_to_Byte_Android(reply))
} } catch (e: Exception) {
} logcb.accept("Failed to send reply to $key, Message : $e")
} }
} }
} }
}
} }
logcb.accept("Finished communicatiing with $key")
socketMap.remove(key)
} }
} catch (e : Exception){
logcb.accept("Exception in communication with $key, Message : ${e.message}")
} }
logcb.accept("Finished communicatiing with $key")
CloseSocket(socket)
socketMap.remove(key)
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
@@ -102,6 +107,14 @@ class TCP_Android_Command_Server {
return false return false
} }
private fun CloseSocket(socket : Socket) {
try {
socket.close()
} catch (e: Exception) {
Logger.error { "Failed to close socket, Message : ${e.message}" }
}
}
/** /**
* Convert a String to ByteArray in prefix AsyncStream format in B4X * Convert a String to ByteArray in prefix AsyncStream format in B4X
* @param str The input string * @param str The input string
@@ -109,8 +122,8 @@ class TCP_Android_Command_Server {
*/ */
private fun String_to_Byte_Android(str: String): ByteArray { private fun String_to_Byte_Android(str: String): ByteArray {
if (ValidString(str)) { if (ValidString(str)) {
val len = str.length val bytes = str.toByteArray(Charsets.UTF_8)
val bytes = str.toByteArray() val len = bytes.size
return ByteBuffer.allocate(len + 4) return ByteBuffer.allocate(len + 4)
.order(ByteOrder.LITTLE_ENDIAN) .order(ByteOrder.LITTLE_ENDIAN)
.putInt(len) .putInt(len)
@@ -128,14 +141,15 @@ class TCP_Android_Command_Server {
*/ */
private fun process_command(key: String, cmd: String, cb: Consumer<String>) { private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
Logger.info { "Command from $key : $cmd" } Logger.info { "Command from $key : $cmd" }
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }.map { it.uppercase() } val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }
when (parts[0]) { when (parts[0]) {
"GETLOGIN" -> { "GETLOGIN" -> {
// Android login request // Android login request
val username = parts.getOrElse(1) { "" } val username = parts.getOrElse(1) { "" }
val password = parts.getOrElse(2) { "" } val password = parts.getOrElse(2) { "" }
if (ValidString(username) && ValidString(password)) { if (ValidString(username) && ValidString(password)) {
if (db.userDB.List.any{it.username==username && it.password==password}) { if (db.userDB.List.any{
it.username==username && it.password==password}) {
val existing = listUserLogin.find { it.ip == key} val existing = listUserLogin.find { it.ip == key}
if (existing!=null){ if (existing!=null){
existing.username = username existing.username = username
@@ -155,99 +169,160 @@ class TCP_Android_Command_Server {
} }
} }
"PCMFILE_START","STARTPAGINGAND" -> { "PCMFILE_START" ->{
// start sending PCM data from Android for paging
val size = parts.getOrElse(1) { "0" }.toInt()
val filename = parts.getOrElse(2) { "" }
val zones = parts.getOrElse(3) { "" }.replace(",",";") val zones = parts.getOrElse(3) { "" }.replace(",",";")
if (size>0){
if (ValidString(filename)){
if (ValidString(zones)){
// create paging job
val pj = PagingJob(key, zones)
// ada expected size
pj.expectedSize = size
// masukin ke list
listOnGoingPaging[key] = pj
Logger.info{"PagingJob created for Android $key, zones: $zones, file: ${pj.filePath.absolutePathString()}"}
tcpreceiver.RequestDataFrom(key) {
// push data ke paging job
pj.addData(it, it.size)
}
cb.accept("PCMFILE_START;OK@")
Logger.info{"Android $key start sending PCM data, expecting $size bytes"}
return
} else logcb.accept("PCMFILE_START from Android $key failed, empty zones")
} else logcb.accept("PCMFILE_START from Android $key failed, empty filename")
} else logcb.accept("PCMFILE_START from Android $key failed, invalid size")
cb.accept("PCMFILE_START;NG@")
}
"PCMFILE_STOP" -> {
// stop sending PCM data from Android for paging
val pj = listOnGoingPaging[key]
if (pj!=null) {
listOnGoingPaging.remove(key)
tcpreceiver.StopRequestDataFrom(key)
// get remaining data
val data = pj.GetData()
pj.Close()
if (data.size==pj.expectedSize){
Logger.info { "Paging job closed from Android $key, total bytes received ${data.size}, writing to file ${pj.filePath.absolutePathString()}" }
val result = audioPlayer.WavWriter(data, pj.filePath.absolutePathString(), true)
if (result.success) {
val qp = QueuePaging(
0u,
LocalDateTime.now().format(datetimeformat1),
"ANDROID",
"PAGING",
pj.filePath.absolutePathString(),
pj.broadcastzones
)
if (db.queuepagingDB.Add(qp)) {
db.queuepagingDB.Resort()
logcb.accept("Paging audio inserted to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
cb.accept("PCMFILE_STOP;OK@")
return
} else logcb.accept("Failed to insert paging audio to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
} else logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : ${result.message}")
} else logcb.accept("PCMFILE_STOP from Android $key received size ${data.size} does not match expected ${pj.expectedSize}")
} else logcb.accept("PCMFILE_STOP from Android $key failed, no ongoing PCM data receiving")
cb.accept("PCMFILE_STOP;NG@")
}
"STARTPAGINGAND" -> {
// Start Paging request from IPM
val zones = parts.getOrElse(1) { "" }.replace(",",";")
if (ValidString(zones)){ if (ValidString(zones)){
// create pagingjob // create pagingjob
val pj = PagingJob(key, zones) val pj = PagingJob(key, zones)
// masukin ke list // masukin ke list
listOnGoingPaging[key] = pj listOnGoingPaging[key] = pj
Logger.info{"PagingJob created for IPM $key, zones: $zones, file: ${pj.filePath.absolutePathString()}"}
// start minta data dari udpreceiver // start minta data dari udpreceiver
udpreceiver.RequestDataFrom(key){ udpreceiver.RequestDataFrom(key){
// push data ke paging job // push data ke paging job
pj.addData(it, it.size) pj.addData(it, it.size)
} }
logcb.accept("Paging started from Android $key") logcb.accept("Paging started from IPM $key")
cb.accept(parts[0]+";OK@") cb.accept("STARTPAGINGAND;OK@")
return return
} else logcb.accept("Paging start from Android $key failed, empty zones") } else logcb.accept("Paging start from IPM $key failed, empty zones")
cb.accept(parts[0]+";NG@") cb.accept("STARTPAGINGAND;NG@")
} }
"PCMFILE_STOP","STOPPAGINGAND" -> { "STOPPAGINGAND" -> {
// stop paging request from IPM
val pj = listOnGoingPaging[key] val pj = listOnGoingPaging[key]
if (pj!=null){ if (pj!=null){
listOnGoingPaging.remove(key) listOnGoingPaging.remove(key)
udpreceiver.StopRequestDataFrom(key) udpreceiver.StopRequestDataFrom(key)
logcb.accept("Paging stopped from Android $key") logcb.accept("Paging stopped from IPM $key")
cb.accept(parts[0]+";OK@")
// get remaining data // get remaining data
val data = pj.GetData() val data = pj.GetData()
pj.Close() pj.Close()
audioPlayer.WavWriter(data, pj.filePath.absolutePathString()){ Logger.info{"Paging job closed from IPM $key, total bytes received ${data.size}, writing to file ${pj.filePath.absolutePathString()}"}
success, message -> val result = audioPlayer.WavWriter(data, pj.filePath.absolutePathString(), true)
if (success){ if (result.success){
// insert to paging queue val qp = QueuePaging(
val qp = QueuePaging( 0u,
0u, LocalDateTime.now().format(datetimeformat1),
LocalDateTime.now().format(datetimeformat1), "IPM",
"PAGING", "PAGING",
"NORMAL", pj.filePath.absolutePathString(),
pj.filePath.absolutePathString(), pj.broadcastzones
pj.broadcastzones )
) if (db.queuepagingDB.Add(qp)){
if (db.queuepagingDB.Add(qp)){ db.queuepagingDB.Resort()
logcb.accept("Paging audio inserted to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}") logcb.accept("Paging audio inserted to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}")
cb.accept(parts[0]+";OK@") cb.accept("STOPPAGINGAND;OK@")
} else { return
logcb.accept("Failed to insert paging audio to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}") } else logcb.accept("Failed to insert paging audio to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}")
cb.accept(parts[0]+";NG@") } else logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : ${result.message}")
} } else logcb.accept("Paging stop from IPM $key failed, no ongoing paging")
cb.accept("STOPPAGINGAND;NG@")
} else {
logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : $message")
cb.accept(parts[0]+";NG@")
}
}
} else {
logcb.accept("Paging stop from Android $key failed, no ongoing paging")
cb.accept(parts[0]+";NG@")
}
} }
"CANCELPAGINGAND" -> { "CANCELPAGINGAND" -> {
// cancel paging request from IPM
val pj = listOnGoingPaging[key] val pj = listOnGoingPaging[key]
if (pj!=null){ if (pj!=null){
pj.Close() pj.Close()
listOnGoingPaging.remove(key) listOnGoingPaging.remove(key)
udpreceiver.StopRequestDataFrom(key) udpreceiver.StopRequestDataFrom(key)
logcb.accept("Paging from Android $key cancelled") logcb.accept("Paging from IPM $key cancelled")
cb.accept("CANCELPAGINGAND;OK@") cb.accept("CANCELPAGINGAND;OK@")
return return
} else logcb.accept("Paging cancel from Android $key failed, no ongoing paging") } else logcb.accept("Paging cancel from IPM $key failed, no ongoing paging")
cb.accept("CANCELPAGINGAND;NG@") cb.accept("CANCELPAGINGAND;NG@")
} }
"STARTINITIALIZE" -> { "STARTINITIALIZE" -> {
// pengiriman variabel ke Android
val username = parts.getOrElse(1) { "" } val username = parts.getOrElse(1) { "" }
if (ValidString(username)){ if (ValidString(username)){
val userlogin = listUserLogin.find { it.username == username } val userlogin = listUserLogin.find { it.username == username }
if (userlogin != null){ if (userlogin != null){
val userdb = db.userDB.List.find { it.username == username } val userdb = db.userDB.List.find { it.username == username }
if (userdb != null){ if (userdb != null){
//println("Sending initialization data to $key with username $username")
val result = StringBuilder() val result = StringBuilder()
// kirim Zone
result.append("ZONE") result.append("ZONE")
userdb.broadcastzones.split(";").map { it.trim() }.filter { it.isNotBlank() }.forEach { userdb.broadcastzones.split(";").map { it.trim() }.filter { it.isNotBlank() }.forEach {
result.append(";") result.append(";")
result.append(it) result.append(it)
} }
result.append("@") result.append("@")
cb.accept(result.toString())
// kirim MSGTOTAL
result.clear()
val VARMESSAGES = mutableListOf<Messagebank>() val VARMESSAGES = mutableListOf<Messagebank>()
result.append("MSGTOTAL;")
userdb.messagebank_ann_id userdb.messagebank_ann_id
// messagebank_ann_id adalah rentengan ANN_ID (digit) yang dipisah dengan ; // messagebank_ann_id adalah rentengan ANN_ID (digit) yang dipisah dengan ;
.split(";") .split(";")
@@ -255,113 +330,273 @@ class TCP_Android_Command_Server {
.map { it.trim() } .map { it.trim() }
// bukan string kosong antar dua tanda ; // bukan string kosong antar dua tanda ;
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
// beneran digit semua
.filter { xx -> xx.all{it.isDigit()} }
// iterasi setiap ANN_ID // iterasi setiap ANN_ID
.forEach { annid -> .forEach { annid ->
// masukin ke VARMESSAGES yang unik secara ANN_ID dan Language // masukin ke VARMESSAGES yang unik secara ANN_ID dan Language
val xx = db.messageDB.List val xx = db.messageDB.List
.filter{ it.ANN_ID == annid.toUInt() } .filter{ it.ANN_ID == annid.toUInt() }
.distinctBy { it.Language } .distinctBy { it.ANN_ID }
VARMESSAGES.addAll(xx) VARMESSAGES.addAll(xx)
} }
result.append("MSGTOTAL;").append(VARMESSAGES.size).append("@") result.append(VARMESSAGES.size).append("@")
// VAR AP TOTAL
val VARAPTOTAL = mutableListOf<Soundbank>()
val sb_split = userdb.soundbank_tags.split(";").map { it.trim() }.filter { it.isNotBlank() }
sb_split.forEach {
val sb = db.Find_Soundbank_AirplaneName(it).firstOrNull()
if (sb != null) VARAPTOTAL.add(sb)
}
result.append("VARAPTOTAL;").append(VARAPTOTAL.size).append("@")
// VAR CITY TOTAL
val VARCITYTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_City(it).firstOrNull()
if (sb != null) VARCITYTOTAL.add(sb)
}
result.append("VARCITYTOTAL;").append(VARCITYTOTAL.size).append("@")
// VAR PLACES TOTAL
val VARPLACESTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Places(it).firstOrNull()
if (sb != null) VARPLACESTOTAL.add(sb)
}
result.append("VARPLACESTOTAL;").append(VARPLACESTOTAL.size).append("@")
// VAR SHALAT TOTAL
val VARSHALATTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Shalat(it).firstOrNull()
if (sb != null) VARSHALATTOTAL.add(sb)
}
result.append("VARSHALATTOTAL;").append(VARSHALATTOTAL.size).append("@")
// VAR SEQUENCE TOTAL
val VARSEQUENCETOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Sequence(it).firstOrNull()
if (sb != null) VARSEQUENCETOTAL.add(sb)
}
// VAR REASON TOTAL
val VARREASONTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Reason(it).firstOrNull()
if (sb != null) VARREASONTOTAL.add(sb)
}
result.append("VARREASONTOTAL;").append(VARREASONTOTAL.size).append("@")
// VAR PROCEDURE TOTAL
val VARPROCEDURETOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Procedure(it).firstOrNull()
if (sb != null) VARPROCEDURETOTAL.add(sb)
}
result.append("VARPROCEDURETOTAL;").append(VARPROCEDURETOTAL.size).append("@")
// send to sender
cb.accept(result.toString()) cb.accept(result.toString())
// kirim VARAPTOTAL
result.clear() result.clear()
result.append("VARAPTOTAL;")
val VARAPTOTAL = mutableListOf<Soundbank>()
userdb.airline_tags
.split(";")
.map { it.trim() }
.filter { it.isNotBlank() }
.forEach { al ->
val sb = db.soundDB.List
.filter { it.Category.equals(Category.Airplane_Name.name, true) }
.filter { it.TAG.equals(al, true)}
.distinctBy { it.TAG }
VARAPTOTAL.addAll(sb)
}
result.append(VARAPTOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARCITYTOTAL
result.clear()
result.append("VARCITYTOTAL;")
val VARCITYTOTAL = mutableListOf<Soundbank>()
userdb.city_tags
.split(";")
.map { it.trim() }
.filter { it.isNotBlank() }
.forEach { ct ->
val sb = db.soundDB.List
.filter { it.Category.equals(Category.City.name, true) }
.filter { it.TAG.equals(ct, true)}
.distinctBy { it.TAG }
VARCITYTOTAL.addAll(sb)
}
result.append(VARCITYTOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARPLACESTOTAL
result.clear()
result.append("VARPLACESTOTAL;")
val VARPLACESTOTAL = mutableListOf<Soundbank>()
db.soundDB.List
.filter { it.Category.equals(Category.Places.name, true) }
.distinctBy { it.TAG }
.forEach {
VARPLACESTOTAL.add(it)
}
result.append(VARPLACESTOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARSHALATTOTAL
result.clear()
result.append("VARSHALATTOTAL;")
val VARSHALATTOTAL = mutableListOf<Soundbank>()
db.soundDB.List
.filter { it.Category.equals(Category.Shalat.name, true) }
.distinctBy { it.TAG }
.forEach {
VARSHALATTOTAL.add(it)
}
result.append(VARSHALATTOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARSEQUENCETOTAL
result.clear()
result.append("VARSEQUENCETOTAL;")
val VARSEQUENCETOTAL = mutableListOf<Soundbank>()
db.soundDB.List
.filter { it.Category.equals(Category.Sequence.name, true) }
.distinctBy { it.TAG }
.forEach {
VARSEQUENCETOTAL.add(it)
}
result.append(VARSEQUENCETOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARREASONTOTAL
result.clear()
result.append("VARREASONTOTAL;")
val VARREASONTOTAL = mutableListOf<Soundbank>()
db.soundDB.List
.filter { it.Category.equals(Category.Reason.name, true) }
.distinctBy { it.TAG }
.forEach {
VARREASONTOTAL.add(it)
}
result.append(VARREASONTOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARPROCEDURETOTAL
val VARPROCEDURETOTAL = mutableListOf<Soundbank>()
result.clear()
result.append("VARPROCEDURETOTAL;")
db.soundDB.List
.filter { it.Category.equals(Category.Procedure.name, true) }
.distinctBy { it.TAG }
.forEach {
VARPROCEDURETOTAL.add(it)
}
result.append(VARPROCEDURETOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARGATETOTAL
val VARGATETOTAL = mutableListOf<Soundbank>()
result.clear()
result.append("VARGATETOTAL;")
db.soundDB.List
.filter { it.Category.equals(Category.Gate.name, true) }
.distinctBy { it.TAG }
.forEach {
VARGATETOTAL.add(it)
}
result.append(VARGATETOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARCOMPENSATIONTOTAL
result.clear()
result.append("VARCOMPENSATIONTOTAL;")
val VARCOMPENSATIONTOTAL = mutableListOf<Soundbank>()
db.soundDB.List
.filter { it.Category.equals(Category.Compensation.name, true) }
.distinctBy { it.TAG }
.forEach {
VARCOMPENSATIONTOTAL.add(it)
}
result.append(VARCOMPENSATIONTOTAL.size).append("@")
cb.accept(result.toString())
// kirim VARGREETINGTOTAL
result.clear()
result.append("VARGREETINGTOTAL;")
val VARGREETINGTOTAL = mutableListOf<Soundbank>()
db.soundDB.List
.filter { it.Category.equals(Category.Greeting.name, true) }
.distinctBy { it.TAG }
.forEach {
VARGREETINGTOTAL.add(it)
}
result.append(VARGREETINGTOTAL.size).append("@")
cb.accept(result.toString())
//Append MSG, for Android only Indonesia and English //Append MSG, for Android only Indonesia and English
VARMESSAGES.groupBy { it.ANN_ID }.forEach { (ann_id, value) -> if (VARMESSAGES.isNotEmpty()) {
result.append("MSG;").append(ann_id) result.clear()
result.append(";") VARMESSAGES.forEachIndexed { index, msg ->
value.find { it.Language== Language.INDONESIA.name }?.let {result.append(it.Message_Detail)} ?: result.append("NA")
result.append(";") val ann_id = msg.ANN_ID
value.find {it.Language== Language.ENGLISH.name }?.let {result.append(it.Message_Detail)} ?: result.append("NA") val msg_indo = db.messageDB.List.find {
result.append("@") it.ANN_ID == ann_id && it.Language.equals(
Language.INDONESIA.name,
true
)
}
val msg_eng = db.messageDB.List.find {
it.ANN_ID == ann_id && it.Language.equals(
Language.ENGLISH.name,
true
)
}
val description = msg_indo?.Description ?: msg_eng?.Description ?: "UNKNOWN"
result.append("MSG;$index;$ann_id;$description;")
result.append(msg_indo?.Message_Detail ?:"").append(";")
result.append(msg_eng?.Message_Detail ?:"").append("@")
}
cb.accept(result.toString())
} }
// append VARAP // append VARAP
VARAPTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> if (VARAPTOTAL.isNotEmpty()) {
result.append("VARAP;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") result.clear()
VARAPTOTAL.forEachIndexed { index, sb ->
result.append("VARAP;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
} }
// append VARCITY // append VARCITY
VARCITYTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> if (VARCITYTOTAL.isNotEmpty()) {
result.append("VARCITY;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") result.clear()
VARCITYTOTAL.forEachIndexed { index, sb ->
result.append("VARCITY;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
} }
// append VARPLACES // append VARPLACES
VARPLACESTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> if (VARPLACESTOTAL.isNotEmpty()) {
result.append("VARPLACES;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") result.clear()
VARPLACESTOTAL.forEachIndexed { index, sb ->
result.append("VARPLACES;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
} }
// append VARSHALAT // append VARSHALAT
VARSHALATTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> if (VARSHALATTOTAL.isNotEmpty()) {
result.append("VARSHALAT;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") result.clear()
VARSHALATTOTAL.forEachIndexed { index, sb ->
result.append("VARSHALAT;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
} }
// append VARSEQUENCE
VARSEQUENCETOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARSEQUENCE;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARREASON
VARREASONTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARREASON;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARPROCEDURE
VARPROCEDURETOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARPROCEDURE;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// send to sender
cb.accept(result.toString())
logcb.accept("All variables sent to $key with username $username")
// append VARSEQUENCE
if (VARSEQUENCETOTAL.isNotEmpty()) {
result.clear()
VARSEQUENCETOTAL.forEachIndexed { index, sb ->
result.append("VARSEQUENCE;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARREASON
if (VARREASONTOTAL.isNotEmpty()) {
result.clear()
VARREASONTOTAL.forEachIndexed { index, sb ->
result.append("VARREASON;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARPROCEDURE
if (VARPROCEDURETOTAL.isNotEmpty()) {
result.clear()
VARPROCEDURETOTAL.forEachIndexed { index, sb ->
result.append("VARPROCEDURE;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARGATE
if (VARGATETOTAL.isNotEmpty()) {
result.clear()
VARGATETOTAL.forEachIndexed { index, sb ->
result.append("VARGATE;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARCOMPENSATION
if (VARCOMPENSATIONTOTAL.isNotEmpty()) {
result.clear()
VARCOMPENSATIONTOTAL.forEachIndexed { index, sb ->
result.append("VARCOMPENSATION;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARGREETING
if (VARGREETINGTOTAL.isNotEmpty()) {
result.clear()
VARGREETINGTOTAL.forEachIndexed { index, sb ->
result.append("VARGREETING;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
logcb.accept("All variables sent to $key with username $username")
return return
} else logcb.accept("STARTINITIALIZE failed from $key with username $username not found in userDB") } else logcb.accept("STARTINITIALIZE failed from $key with username $username not found in userDB")
} else logcb.accept("STARTINITIALIZE failed from $key with unregistered username $username") } else logcb.accept("STARTINITIALIZE failed from $key with unregistered username $username")
@@ -372,8 +607,11 @@ class TCP_Android_Command_Server {
"BROADCASTAND" -> { "BROADCASTAND" -> {
// semi auto dari android, masukin ke queue table // semi auto dari android, masukin ke queue table
val desc = parts.getOrElse(1) { "" } val desc = parts.getOrElse(1) { "" }
// language bisa lebih dari satu, dipisah dengan koma
val lang = parts.getOrElse(2) { "" }.replace(",",";") val lang = parts.getOrElse(2) { "" }.replace(",",";")
// tags bisa lebih dari satu, dipisah dengan spasi
val tags = parts.getOrElse(3) { "" }.replace(",",";") val tags = parts.getOrElse(3) { "" }.replace(",",";")
// zone bisa lebih dari satu, dipisah dengan koma
val zone = parts.getOrElse(4) { "" }.replace(",",";") val zone = parts.getOrElse(4) { "" }.replace(",",";")
if (ValidString(desc)){ if (ValidString(desc)){
if (ValidString(lang)){ if (ValidString(lang)){
@@ -391,6 +629,7 @@ class TCP_Android_Command_Server {
lang lang
) )
if (db.queuetableDB.Add(qt)){ if (db.queuetableDB.Add(qt)){
db.queuetableDB.Resort()
logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} inserted. Message: $desc;$lang;$tags;$zone") logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} inserted. Message: $desc;$lang;$tags;$zone")
cb.accept("BROADCASTAND;OK@") cb.accept("BROADCASTAND;OK@")
return return
@@ -399,7 +638,7 @@ class TCP_Android_Command_Server {
} else logcb.accept("Broadcsast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty tags") } else logcb.accept("Broadcsast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty tags")
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty language") } else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty language")
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty description") } else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty description")
cb.accept("NG@") cb.accept("BROADCASTAND;NG@")
} }

View File

@@ -14,5 +14,8 @@ enum class Category(name: String) {
Birthday("Birthday"), Birthday("Birthday"),
Reason("Reason"), Reason("Reason"),
Sequence("Sequence"), Sequence("Sequence"),
Procedure("Procedure"); Procedure("Procedure"),
Gate("Gate"),
Greeting("Greeting"),
Compensation("Compensation");
} }

View File

@@ -1,73 +0,0 @@
package content
import audio.AudioFileInfo
/**
* Class to manage loaded content in the application.
* This class provides methods to retrieve and add soundbank data based on specific criteria.
*/
@Suppress("unused")
class ContentCache {
private val contentList = ArrayList<SoundbankData>()
/**
* Clears all loaded content from the cache.
*/
fun Clear(){
contentList.clear()
}
/**
* Removes the specified SoundbankData from the content list.
* @param SoundbankData The SoundbankData to be removed.
*/
fun Remove(SoundbankData: SoundbankData){
contentList.remove(SoundbankData)
}
/**
* Removes the specified SoundbankData from the content list based on tag, category, language, and voiceType.
* @param tag The tag of the SoundbankData to be removed.
* @param category The category of the SoundbankData to be removed.
* @param language The language of the SoundbankData to be removed.
* @param voiceType The voice type of the SoundbankData to be removed.
*/
fun Remove(tag: String, category: Category, language: Language, voiceType: VoiceType){
val existing = Get(tag, category, language, voiceType)
if (existing!=null) contentList.remove(existing)
}
/**
* Get the specified SoundbankData from tag, category, language, and voiceType.
* @return SoundbankData if found, null otherwise.
*/
fun Get(tag: String, category: Category, language: Language, voiceType: VoiceType): SoundbankData? {
return contentList.find {
it.TAG == tag &&
it.Category == category &&
it.Language == language &&
it.VoiceType == voiceType &&
it.audio.isValid()
}
}
/**
* Adds a new soundbank to the content list if it does not already exist.
*
* @param tag The unique identifier for the soundbank.
* @param category The category of the soundbank.
* @param language The language of the soundbank.
* @param voiceType The voice type of the soundbank.
* @param audio The audio file information for the soundbank.
* @return True if the soundbank was added, false if it already exists.
*/
fun Add(tag: String, category: Category, language: Language, voiceType: VoiceType, audio: AudioFileInfo): Boolean {
val existing = Get(tag, category, language, voiceType)
if (existing!=null) return false
contentList.add(SoundbankData(tag, category, language, voiceType, audio))
return true
}
}

View File

@@ -7,10 +7,10 @@ package content
*/ */
@Suppress("unused") @Suppress("unused")
enum class Language(name: String) { enum class Language(name: String) {
INDONESIA("Indonesia"), INDONESIA("INDONESIA"),
ENGLISH("English"), ENGLISH("ENGLISH"),
LOCAL("Local"), LOCAL("LOCAL"),
JAPANESE("Japanese"), JAPANESE("JAPANESE"),
CHINESE("Chinese"), CHINESE("CHINESE"),
ARABIC("Arabic"); ARABIC("ARABIC");
} }

View File

@@ -0,0 +1,15 @@
package content
class NetworkInformation(val name: String, val displayName: String, val macAddress: String) {
var ipV4addr: MutableList<String> = mutableListOf()
var ipV6addr: MutableList<String> = mutableListOf()
var bytesSent: Long = 0
var bytesRecv: Long = 0
var packetsSent: Long = 0
var packetsRecv: Long = 0
var speed: Long = 0 // in bits per second
var updateStamp : Long = 0 // epoch time in milliseconds, buat hitung speed
var txSpeed: Long = 0
var rxSpeed: Long = 0
}

View File

@@ -1,4 +1,28 @@
package database package database
@Suppress("unused") @Suppress("unused")
data class BroadcastZones(var index: UInt, var description: String, var SoundChannel: String, var id: String, var bp: String) data class BroadcastZones(var index: UInt, var description: String, var SoundChannel: String, var id: String, var bp: String){
/**
* Check if all fields are not empty
*/
fun isNotEmpty(): Boolean{
if (description.isNotEmpty()){
if (SoundChannel.isNotEmpty()){
if (id.isNotEmpty()){
if (bp.isNotEmpty()){
return true
}
}
}
}
return false
}
/**
* Return a string representation of the BroadcastZones object.
*/
override fun toString(): String {
return "BroadcastZones(index=$index, description='$description', SoundChannel='$SoundChannel', id='$id', bp='$bp')"
}
}

View File

@@ -1,3 +0,0 @@
package database
data class BroadcastZonesHtml(var index: UInt, var description: String, var SoundChannel: String, var Box: String, var Relay: String)

View File

@@ -1,4 +0,0 @@
package database
@Suppress("unused")
data class IpZones(var index:UInt, var description: String, var ip: String)

View File

@@ -1,4 +1,22 @@
package database package database
@Suppress("unused") data class LanguageLink(var index: UInt, var TAG: String, var Language: String){
data class LanguageLink(var index: UInt, var TAG: String, var Language: String)
/**
* Check if the LanguageLink object has non-empty TAG and Language fields.
* @return true if both TAG and Language are not empty, false otherwise.
*/
fun isNotEmpty() : Boolean {
return TAG.isNotEmpty() && Language.isNotEmpty()
}
/**
* Returns a string representation of the LanguageLink object.
* @return A string in the format "LanguageLink(index=..., TAG='...', Language='...')".
*/
override fun toString(): String {
return "LanguageLink(index=$index, TAG='$TAG', Language='$Language')"
}
}

View File

@@ -1,4 +1,9 @@
package database package database
@Suppress("unused") data class LogSemiauto(val index: UInt, val date: String, val time: String, val source: String, val description: String){
data class LogSemiauto(val index: UInt, val date: String, val time: String, val source: String, val description: String)
override fun toString(): String {
return "$date $time [$source] $description"
}
}

View File

@@ -12,7 +12,6 @@ import org.tinylog.Logger
import java.sql.Connection import java.sql.Connection
import java.sql.DriverManager import java.sql.DriverManager
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.math.max
/** /**
* A class to manage a connection to a MariaDB database. * A class to manage a connection to a MariaDB database.
@@ -105,7 +104,7 @@ class MariaDB(
List.clear() List.clear()
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}") val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName} ORDER BY Category, Language, VoiceType, TAG")
while (resultSet?.next() == true) { while (resultSet?.next() == true) {
val soundbank = Soundbank( val soundbank = Soundbank(
resultSet.getLong("index").toUInt(), resultSet.getLong("index").toUInt(),
@@ -221,13 +220,15 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM ${super.dbName} ORDER BY Description ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM ${super.dbName} ORDER BY Category, Language, VoiceType, TAG ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by Description" as Any) Logger.info("${super.dbName} table resorted by Description" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -421,12 +422,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) SELECT Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS FROM ${super.dbName} ORDER BY ANN_ID ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) SELECT Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS FROM ${super.dbName} ORDER BY ANN_ID ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) SELECT Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) SELECT Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by Description" as Any) Logger.info("${super.dbName} table resorted by Description" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -616,12 +619,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (TAG, Language) SELECT TAG, Language FROM ${super.dbName} ORDER BY TAG") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (TAG, Language) SELECT TAG, Language FROM ${super.dbName} ORDER BY TAG")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (TAG, Language) SELECT TAG, Language FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (TAG, Language) SELECT TAG, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by TAG" as Any) Logger.info("${super.dbName} table resorted by TAG" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -834,12 +839,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language) SELECT Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language FROM ${super.dbName} ORDER BY Day , Time ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language) SELECT Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language FROM ${super.dbName} ORDER BY Day , Time ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language) SELECT Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language) SELECT Description, Day, Time, Soundpath, Repeat, Enable, BroadcastZones, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by Day and Time" as Any) Logger.info("${super.dbName} table resorted by Day and Time" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -988,7 +995,7 @@ class MariaDB(
override fun Add(data: BroadcastZones): Boolean { override fun Add(data: BroadcastZones): Boolean {
try { try {
val statement = val statement =
connection.prepareStatement("INSERT INTO ${super.dbName} (description, SoundChannel, Box, Relay) VALUES (?, ?, ?, ?)") connection.prepareStatement("INSERT INTO ${super.dbName} (description, SoundChannel, id, bp) VALUES (?, ?, ?, ?)")
statement?.setString(1, data.description) statement?.setString(1, data.description)
statement?.setString(2, data.SoundChannel) statement?.setString(2, data.SoundChannel)
statement?.setString(3, data.id) statement?.setString(3, data.id)
@@ -1010,7 +1017,7 @@ class MariaDB(
try { try {
connection.autoCommit = false connection.autoCommit = false
val sql = val sql =
"INSERT INTO ${super.dbName} (description, SoundChannel, Box, Relay) VALUES (?, ?, ?, ?)" "INSERT INTO ${super.dbName} (description, SoundChannel, id, bp) VALUES (?, ?, ?, ?)"
val statement = connection.prepareStatement(sql) val statement = connection.prepareStatement(sql)
for (bz in data) { for (bz in data) {
statement.setString(1, bz.description) statement.setString(1, bz.description)
@@ -1033,7 +1040,7 @@ class MariaDB(
override fun UpdateByIndex(index: Int, data: BroadcastZones): Boolean { override fun UpdateByIndex(index: Int, data: BroadcastZones): Boolean {
try { try {
val statement = val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET description = ?, SoundChannel = ?, Box = ?, Relay = ? WHERE `index` = ?") connection.prepareStatement("UPDATE ${super.dbName} SET description = ?, SoundChannel = ?, id = ?, bp = ? WHERE `index` = ?")
statement?.setString(1, data.description) statement?.setString(1, data.description)
statement?.setString(2, data.SoundChannel) statement?.setString(2, data.SoundChannel)
statement?.setString(3, data.id) statement?.setString(3, data.id)
@@ -1055,12 +1062,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (description, SoundChannel, Box, Relay) SELECT description, SoundChannel, Box, Relay FROM ${super.dbName} ORDER BY description ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (description, SoundChannel, id, bp) SELECT description, SoundChannel, id, bp FROM ${super.dbName} ORDER BY description ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (description, SoundChannel, Box, Relay) SELECT description, SoundChannel, Box, Relay FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (description, SoundChannel, id, bp) SELECT description, SoundChannel, id, bp FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by description" as Any) Logger.info("${super.dbName} table resorted by description" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -1076,7 +1085,7 @@ class MariaDB(
val sheet = workbook.getSheet("BroadcastZones") val sheet = workbook.getSheet("BroadcastZones")
?: throw Exception("No sheet named 'BroadcastZones' found") ?: throw Exception("No sheet named 'BroadcastZones' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found") val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf("Index", "description", "SoundChannel", "Box", "Relay") val headers = arrayOf("Index", "description", "SoundChannel", "id", "bp")
for ((colIndex, header) in headers.withIndex()) { for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found") val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found") if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
@@ -1089,9 +1098,9 @@ class MariaDB(
val row = sheet.getRow(rowIndex) ?: continue val row = sheet.getRow(rowIndex) ?: continue
val description = row.getCell(1)?.stringCellValue ?: continue val description = row.getCell(1)?.stringCellValue ?: continue
val soundChannel = row.getCell(2)?.stringCellValue ?: continue val soundChannel = row.getCell(2)?.stringCellValue ?: continue
val box = row.getCell(3)?.stringCellValue ?: continue val id = row.getCell(3)?.stringCellValue ?: continue
val relay = row.getCell(4)?.stringCellValue ?: continue val bp = row.getCell(4)?.stringCellValue ?: continue
val broadcastZone = BroadcastZones(0u, description, soundChannel, box, relay) val broadcastZone = BroadcastZones(0u, description, soundChannel, id, bp)
_broadcastZonesList.add(broadcastZone) _broadcastZonesList.add(broadcastZone)
} }
return AddAll(_broadcastZonesList) return AddAll(_broadcastZonesList)
@@ -1108,7 +1117,7 @@ class MariaDB(
val workbook = XSSFWorkbook() val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("BroadcastZones") val sheet = workbook.createSheet("BroadcastZones")
val headerRow = sheet.createRow(0) val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "description", "SoundChannel", "Box", "Relay") val headers = arrayOf("Index", "description", "SoundChannel", "id", "bp")
for ((colIndex, header) in headers.withIndex()) { for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex) val cell = headerRow.createCell(colIndex)
cell.setCellValue(header) cell.setCellValue(header)
@@ -1119,8 +1128,8 @@ class MariaDB(
row.createCell(0).setCellValue(resultSet.getString("index")) row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("description")) row.createCell(1).setCellValue(resultSet.getString("description"))
row.createCell(2).setCellValue(resultSet.getString("SoundChannel")) row.createCell(2).setCellValue(resultSet.getString("SoundChannel"))
row.createCell(3).setCellValue(resultSet.getString("Box")) row.createCell(3).setCellValue(resultSet.getString("id"))
row.createCell(4).setCellValue(resultSet.getString("Relay")) row.createCell(4).setCellValue(resultSet.getString("bp"))
} }
for (i in headers.indices) { for (i in headers.indices) {
sheet.autoSizeColumn(i) sheet.autoSizeColumn(i)
@@ -1145,7 +1154,7 @@ class MariaDB(
"SB_TAGS VARCHAR(1024)," + // Comma-separated soundbank tags "SB_TAGS VARCHAR(1024)," + // Comma-separated soundbank tags
"BroadcastZones VARCHAR(1024) NOT NULL," + // Comma-separated broadcast zones "BroadcastZones VARCHAR(1024) NOT NULL," + // Comma-separated broadcast zones
"`Repeat` INT NOT NULL," + // Number of repeats "`Repeat` INT NOT NULL," + // Number of repeats
"Language VARCHAR(45) NOT NULL" + // Language of the message "Language VARCHAR(100) NOT NULL" + // Language of the message
")" ")"
super.Create(tabledefinition) super.Create(tabledefinition)
} }
@@ -1179,7 +1188,7 @@ class MariaDB(
override fun Add(data: QueueTable): Boolean { override fun Add(data: QueueTable): Boolean {
try { try {
val statement = connection.prepareStatement( val statement = connection.prepareStatement(
"INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" "INSERT INTO ${super.dbName} (`Date_Time`, `Source`, `Type`, `Message`, `SB_TAGS`, `BroadcastZones`, `Repeat`, `Language`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
) )
statement?.setString(1, data.Date_Time) statement?.setString(1, data.Date_Time)
statement?.setString(2, data.Source) statement?.setString(2, data.Source)
@@ -1191,7 +1200,7 @@ class MariaDB(
statement?.setString(8, data.Language) statement?.setString(8, data.Language)
val rowsAffected = statement?.executeUpdate() val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) { if (rowsAffected != null && rowsAffected > 0) {
Logger.info("QueueTable added: ${data.Message}" as Any) Logger.info("QueueTable added Source=${data.Source} Type=${data.Type} Message=${data.Message}, Languages=${data.Language} Variables=${data.SB_TAGS}, BroadcastZones=${data.BroadcastZones}" as Any)
return true return true
} else { } else {
Logger.warn("No QueueTable entry added for: ${data.Message}" as Any) Logger.warn("No QueueTable entry added for: ${data.Message}" as Any)
@@ -1206,7 +1215,7 @@ class MariaDB(
try { try {
connection.autoCommit = false connection.autoCommit = false
val sql = val sql =
"INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" "INSERT INTO ${super.dbName} (`Date_Time`, `Source`, `Type`, `Message`, `SB_TAGS`, `BroadcastZones`, `Repeat`, `Language`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql) val statement = connection.prepareStatement(sql)
for (qt in data) { for (qt in data) {
statement.setString(1, qt.Date_Time) statement.setString(1, qt.Date_Time)
@@ -1237,12 +1246,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language FROM ${super.dbName} ORDER BY `index` ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language FROM ${super.dbName} ORDER BY `index` ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any) Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -1311,7 +1322,7 @@ class MariaDB(
"Date_Time VARCHAR(45) NOT NULL," + // Date and time of the entry "Date_Time VARCHAR(45) NOT NULL," + // Date and time of the entry
"Source VARCHAR(45) NOT NULL," + // Source of the entry "Source VARCHAR(45) NOT NULL," + // Source of the entry
"Type VARCHAR(45) NOT NULL," + // Type of the entry "Type VARCHAR(45) NOT NULL," + // Type of the entry
"Message VARCHAR(512) NOT NULL," + // Message content "Message VARCHAR(1024) NOT NULL," + // Message content
"BroadcastZones VARCHAR(1024)" + // Comma-separated soundbank tags "BroadcastZones VARCHAR(1024)" + // Comma-separated soundbank tags
")" ")"
super.Create(tabledefinition) super.Create(tabledefinition)
@@ -1330,7 +1341,7 @@ class MariaDB(
resultSet.getString("Source"), resultSet.getString("Source"),
resultSet.getString("Type"), resultSet.getString("Type"),
resultSet.getString("Message"), resultSet.getString("Message"),
resultSet.getString("SB_TAGS"), resultSet.getString("BroadcastZones"),
) )
queueList.add(queuePaging) queueList.add(queuePaging)
List.add(queuePaging) List.add(queuePaging)
@@ -1396,12 +1407,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS) SELECT Date_Time, Source, Type, Message, SB_TAGS FROM ${super.dbName} ORDER BY `index` ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Date_Time, Source, Type, Message, BroadcastZones) SELECT Date_Time, Source, Type, Message, BroadcastZones FROM ${super.dbName} ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS) SELECT Date_Time, Source, Type, Message, SB_TAGS FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, BroadcastZones) SELECT Date_Time, Source, Type, Message, BroadcastZones FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any) Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -1423,7 +1436,7 @@ class MariaDB(
val workbook = XSSFWorkbook() val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("QueuePaging") val sheet = workbook.createSheet("QueuePaging")
val headerRow = sheet.createRow(0) val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "Date_Time", "Source", "Type", "Message", "SB_TAGS") val headers = arrayOf("Index", "Date_Time", "Source", "Type", "Message", "BroadcastZones")
for ((colIndex, header) in headers.withIndex()) { for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex) val cell = headerRow.createCell(colIndex)
cell.setCellValue(header) cell.setCellValue(header)
@@ -1436,7 +1449,7 @@ class MariaDB(
row.createCell(2).setCellValue(resultSet.getString("Source")) row.createCell(2).setCellValue(resultSet.getString("Source"))
row.createCell(3).setCellValue(resultSet.getString("Type")) row.createCell(3).setCellValue(resultSet.getString("Type"))
row.createCell(4).setCellValue(resultSet.getString("Message")) row.createCell(4).setCellValue(resultSet.getString("Message"))
row.createCell(5).setCellValue(resultSet.getString("SB_TAGS")) row.createCell(5).setCellValue(resultSet.getString("BroadcastZones"))
} }
for (i in headers.indices) { for (i in headers.indices) {
sheet.autoSizeColumn(i) sheet.autoSizeColumn(i)
@@ -1557,12 +1570,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (channel, ip) SELECT channel, ip FROM ${super.dbName} ORDER BY `index` ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (channel, ip) SELECT channel, ip FROM ${super.dbName} ORDER BY `index` ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (channel, ip) SELECT channel, ip FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (channel, ip) SELECT channel, ip FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any) Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -1730,7 +1745,6 @@ class MariaDB(
statement?.setString(4, data.description) statement?.setString(4, data.description)
val rowsAffected = statement?.executeUpdate() val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) { if (rowsAffected != null && rowsAffected > 0) {
Logger.info{"Log added : $data"}
return true return true
} else { } else {
Logger.warn{"Failed to add log entry : $data"} Logger.warn{"Failed to add log entry : $data"}
@@ -1771,19 +1785,21 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate( statement?.executeUpdate(
"INSERT INTO temp_${super.dbName} (datenya, timenya, machine, description) " + "INSERT INTO $tempdb_name (datenya, timenya, machine, description) " +
"SELECT datenya, timenya, machine, description FROM ${super.dbName} " + "SELECT datenya, timenya, machine, description FROM ${super.dbName} " +
"ORDER BY datenya , timenya , machine " "ORDER BY datenya , timenya , machine "
) )
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate( statement?.executeUpdate(
"INSERT INTO ${super.dbName} (datenya, timenya, machine, description) " + "INSERT INTO ${super.dbName} (datenya, timenya, machine, description) " +
"SELECT datenya, timenya, machine, description FROM temp_${super.dbName}" "SELECT datenya, timenya, machine, description FROM $tempdb_name"
) )
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by datenya, timenya, machine" as Any) Logger.info("${super.dbName} table resorted by datenya, timenya, machine" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -1840,7 +1856,8 @@ class MariaDB(
"username VARCHAR(100) NOT NULL," + "username VARCHAR(100) NOT NULL," +
"password VARCHAR(100) NOT NULL," + "password VARCHAR(100) NOT NULL," +
"location VARCHAR(100) NOT NULL," + "location VARCHAR(100) NOT NULL," +
"soundbank_tags TEXT NOT NULL,"+ // Comma-separated soundbank tags "airline_tags TEXT NOT NULL,"+ // Comma-separated soundbank tags
"city_tags TEXT NOT NULL,"+ // Comma-separated soundbank tags
"messagebank_ann_id TEXT NOT NULL,"+ // Comma-separated messagebank announcement index "messagebank_ann_id TEXT NOT NULL,"+ // Comma-separated messagebank announcement index
"broadcastzones TEXT NOT NULL"+ // Comma-separated broadcast zones "broadcastzones TEXT NOT NULL"+ // Comma-separated broadcast zones
")" ")"
@@ -1858,7 +1875,8 @@ class MariaDB(
resultSet.getString("username"), resultSet.getString("username"),
resultSet.getString("password"), resultSet.getString("password"),
resultSet.getString("location"), resultSet.getString("location"),
resultSet.getString("soundbank_tags"), resultSet.getString("airline_tags"),
resultSet.getString("city_tags"),
resultSet.getString("messagebank_ann_id"), resultSet.getString("messagebank_ann_id"),
resultSet.getString("broadcastzones") resultSet.getString("broadcastzones")
) )
@@ -1872,13 +1890,14 @@ class MariaDB(
override fun Add(data: UserDB): Boolean { override fun Add(data: UserDB): Boolean {
try { try {
val statement = connection.prepareStatement("INSERT INTO ${super.dbName} (username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones) VALUES (?, ?, ?, ?, ?, ?)") val statement = connection.prepareStatement("INSERT INTO ${super.dbName} (username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones) VALUES (?, ?, ?, ?,?, ?, ?)")
statement?.setString(1, data.username) statement?.setString(1, data.username)
statement?.setString(2, data.password) statement?.setString(2, data.password)
statement?.setString(3, data.location) statement?.setString(3, data.location)
statement?.setString(4, data.soundbank_tags) statement?.setString(4, data.airline_tags)
statement?.setString(5, data.messagebank_ann_id) statement?.setString(5, data.city_tags)
statement?.setString(6, data.broadcastzones) statement?.setString(6, data.messagebank_ann_id)
statement?.setString(7, data.broadcastzones)
val rowsAffected = statement?.executeUpdate() val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) { if (rowsAffected != null && rowsAffected > 0) {
Logger.info("User added: ${data.username}" as Any) Logger.info("User added: ${data.username}" as Any)
@@ -1895,15 +1914,16 @@ class MariaDB(
override fun AddAll(data: ArrayList<UserDB>): Boolean { override fun AddAll(data: ArrayList<UserDB>): Boolean {
return try { return try {
connection.autoCommit = false connection.autoCommit = false
val sql = "INSERT INTO ${super.dbName} (username, password, location,soundbank_tags, messagebank_ann_id, broadcastzones) VALUES (?, ?, ?, ?, ?, ?)" val sql = "INSERT INTO ${super.dbName} (username, password, location,airline_tags,city_tags, messagebank_ann_id, broadcastzones) VALUES (?, ?, ?,?, ?, ?, ?)"
val statement = connection.prepareStatement(sql) val statement = connection.prepareStatement(sql)
for (user in data) { for (user in data) {
statement.setString(1, user.username) statement.setString(1, user.username)
statement.setString(2, user.password) statement.setString(2, user.password)
statement.setString(3, user.location) statement.setString(3, user.location)
statement.setString(4, user.soundbank_tags) statement.setString(4, user.airline_tags)
statement.setString(5, user.messagebank_ann_id) statement.setString(5, user.city_tags)
statement.setString(6, user.broadcastzones) statement.setString(6, user.messagebank_ann_id)
statement.setString(7, user.broadcastzones)
statement.addBatch() statement.addBatch()
} }
statement.executeBatch() statement.executeBatch()
@@ -1919,14 +1939,15 @@ class MariaDB(
override fun UpdateByIndex(index: Int, data: UserDB): Boolean { override fun UpdateByIndex(index: Int, data: UserDB): Boolean {
try { try {
val statement = connection.prepareStatement("UPDATE ${super.dbName} SET username = ?, password = ?, location = ?, soundbank_tags = ?, messagebank_ann_id = ?, broadcastzones = ? WHERE `index` = ?") val statement = connection.prepareStatement("UPDATE ${super.dbName} SET username = ?, password = ?, location = ?, airline_tags = ?,city_tags=?, messagebank_ann_id = ?, broadcastzones = ? WHERE `index` = ?")
statement?.setString(1, data.username) statement?.setString(1, data.username)
statement?.setString(2, data.password) statement?.setString(2, data.password)
statement?.setString(3, data.location) statement?.setString(3, data.location)
statement?.setString(4, data.soundbank_tags) statement?.setString(4, data.airline_tags)
statement?.setString(5, data.messagebank_ann_id) statement?.setString(5, data.city_tags)
statement?.setString(6, data.broadcastzones) statement?.setString(6, data.messagebank_ann_id)
statement?.setLong(7, index.toLong()) statement?.setString(7, data.broadcastzones)
statement?.setLong(8, index.toLong())
val rowsAffected = statement?.executeUpdate() val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) { if (rowsAffected != null && rowsAffected > 0) {
Logger.info("User updated at index $index: ${data.username}" as Any) Logger.info("User updated at index $index: ${data.username}" as Any)
@@ -1943,12 +1964,14 @@ class MariaDB(
override fun Resort(): Boolean { override fun Resort(): Boolean {
try { try {
val statement = connection.createStatement() val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index // use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}") statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones) SELECT username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones FROM ${super.dbName} ORDER BY username ") statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones) SELECT username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones FROM ${super.dbName} ORDER BY username ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}") statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones) SELECT username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones FROM temp_${super.dbName}") statement?.executeUpdate("INSERT INTO ${super.dbName} (username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones) SELECT username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}") statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any) Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list // reload the local list
Get() Get()
@@ -1963,7 +1986,7 @@ class MariaDB(
try { try {
val sheet = workbook.getSheet("User") ?: throw Exception("No sheet named 'User' found") val sheet = workbook.getSheet("User") ?: throw Exception("No sheet named 'User' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found") val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf("Index", "username", "password", "location", "soundbank_tags", "messagebank_ann_id", "broadcastzones") val headers = arrayOf("Index", "username", "password", "location", "airline_tags", "city_tags", "messagebank_ann_id", "broadcastzones")
for ((colIndex, header) in headers.withIndex()) { for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found") val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found") if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
@@ -1977,10 +2000,11 @@ class MariaDB(
val username = row.getCell(1)?.stringCellValue ?: continue val username = row.getCell(1)?.stringCellValue ?: continue
val password = row.getCell(2)?.stringCellValue ?: continue val password = row.getCell(2)?.stringCellValue ?: continue
val location = row.getCell(3)?.stringCellValue ?: continue val location = row.getCell(3)?.stringCellValue ?: continue
val soundbank_tags = row.getCell(4)?.stringCellValue ?: continue val airline_tags = row.getCell(4)?.stringCellValue ?: continue
val messagebank_ann_id = row.getCell(5)?.stringCellValue ?: continue val city_tags = row.getCell(5)?.stringCellValue ?: continue
val broadcastzones = row.getCell(6)?.stringCellValue ?: continue val messagebank_ann_id = row.getCell(6)?.stringCellValue ?: continue
val user = UserDB(0u, username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones) val broadcastzones = row.getCell(7)?.stringCellValue ?: continue
val user = UserDB(0u, username, password, location, airline_tags,city_tags, messagebank_ann_id, broadcastzones)
_userList.add(user) _userList.add(user)
} }
return AddAll(_userList) return AddAll(_userList)
@@ -1997,7 +2021,7 @@ class MariaDB(
val workbook = XSSFWorkbook() val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("User") val sheet = workbook.createSheet("User")
val headerRow = sheet.createRow(0) val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "username", "password", "location", "soundbank_tags", "messagebank_ann_id", "broadcastzones") val headers = arrayOf("Index", "username", "password", "location", "airline_tags","city_tags", "messagebank_ann_id", "broadcastzones")
for ((colIndex, header) in headers.withIndex()) { for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex) val cell = headerRow.createCell(colIndex)
cell.setCellValue(header) cell.setCellValue(header)
@@ -2009,9 +2033,10 @@ class MariaDB(
row.createCell(1).setCellValue(resultSet.getString("username")) row.createCell(1).setCellValue(resultSet.getString("username"))
row.createCell(2).setCellValue(resultSet.getString("password")) row.createCell(2).setCellValue(resultSet.getString("password"))
row.createCell(3).setCellValue(resultSet.getString("location")) row.createCell(3).setCellValue(resultSet.getString("location"))
row.createCell(4).setCellValue(resultSet.getString("soundbank_tags")) row.createCell(4).setCellValue(resultSet.getString("airline_tags"))
row.createCell(5).setCellValue(resultSet.getString("messagebank_ann_id")) row.createCell(5).setCellValue(resultSet.getString("city_tags"))
row.createCell(6).setCellValue(resultSet.getString("broadcastzones")) row.createCell(6).setCellValue(resultSet.getString("messagebank_ann_id"))
row.createCell(7).setCellValue(resultSet.getString("broadcastzones"))
} }
for (i in headers.indices) { for (i in headers.indices) {
sheet.autoSizeColumn(i) sheet.autoSizeColumn(i)
@@ -2248,24 +2273,21 @@ class MariaDB(
* @return a list of Soundbank with Category City and matching tag * @return a list of Soundbank with Category City and matching tag
*/ */
fun Find_Soundbank_City(tag: String) : List<Soundbank>{ fun Find_Soundbank_City(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List return soundDB.List
.filter{ it.Category== Category.City.name } .filter{ it.Category== Category.City.name }
.filter { it.TAG.lowercase()==lowerTag} .filter { it.TAG.equals(tag,true)}
} }
fun Find_Soundbank_AirplaneName(tag: String) : List<Soundbank>{ fun Find_Soundbank_AirplaneName(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List return soundDB.List
.filter{ it.Category== Category.Airplane_Name.name } .filter{ it.Category== Category.Airplane_Name.name }
.filter { it.TAG.lowercase()==lowerTag} .filter { it.TAG.equals(tag,true)}
} }
fun Find_Soundbank_Places(tag: String) : List<Soundbank>{ fun Find_Soundbank_Places(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List return soundDB.List
.filter{ it.Category== Category.Places.name } .filter{ it.Category== Category.Places.name }
.filter { it.TAG.lowercase()==lowerTag} .filter { it.TAG.equals(tag,true)}
} }
fun Find_Soundbank_Shalat(tag: String) : List<Soundbank>{ fun Find_Soundbank_Shalat(tag: String) : List<Soundbank>{
@@ -2276,25 +2298,85 @@ class MariaDB(
} }
fun Find_Soundbank_Sequence(tag: String) : List<Soundbank>{ fun Find_Soundbank_Sequence(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List return soundDB.List
.filter{ it.Category== Category.Sequence.name } .filter{ it.Category== Category.Sequence.name }
.filter { it.TAG.lowercase()==lowerTag} .filter { it.TAG.equals(tag,true)}
} }
fun Find_Soundbank_Reason(tag: String) : List<Soundbank>{ fun Find_Soundbank_Reason(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List return soundDB.List
.filter{ it.Category== Category.Reason.name } .filter{ it.Category== Category.Reason.name }
.filter { it.TAG.lowercase()==lowerTag} .filter { it.TAG.equals(tag,true)}
} }
fun Find_Soundbank_Procedure(tag: String) : List<Soundbank> { fun Find_Soundbank_Procedure(tag: String) : List<Soundbank> {
val lowerTag = tag.lowercase()
return soundDB.List return soundDB.List
.filter { it.Category == Category.Procedure.name } .filter { it.Category == Category.Procedure.name }
.filter { it.TAG.lowercase() == lowerTag } .filter { it.TAG.equals(tag,true) }
}
/**
* Get all distinct airline code tags from soundbank
* @return a list of distinct airline code tags sorted alphabetically
*/
fun Get_AirlineCode_Tags(): List<String> {
return soundDB.List
.filter { it.Category == Category.Airline_Code.name }
.map { it.TAG }
.distinct()
.sorted()
}
/**
* Get all distinct city tags from soundbank
* @return a list of distinct city tags sorted alphabetically
*/
fun Get_City_Tags(): List<String> {
return soundDB.List
.filter { it.Category == Category.City.name }
.map { it.TAG }
.distinct()
.sorted()
}
/**
* Get all distinct message ID from messagebank
* @return a list of distinct ANN_ID sorted numerically
*/
fun Get_MessageID_List(): List<UInt> {
return messageDB.List
.distinctBy { it.ANN_ID }
.map { it.ANN_ID }
.sorted()
}
/**
* Get all distinct broadcast zone descriptions from broadcastDB
* @return a list of distinct broadcast zone descriptions sorted alphabetically
*/
fun Get_BroadcastZone_List(): List<String> {
return broadcastDB.List
.distinctBy { it.description }
.map { it.description }
.sorted()
}
/**
* Get all distinct sound channel from soundchannelDB
* @return a list of distinct sound channel sorted alphabetically
*/
fun Get_SoundChannel_List(): List<String> {
return soundchannelDB.List
.distinctBy { it.channel }
.map { it.channel }
.sorted()
}
/**
* Check if a username already exists in the userDB (case-insensitive)
*/
fun Username_exists(username: String): Boolean {
return userDB.List.any { it.username.equals(username, ignoreCase = true) }
} }

View File

@@ -8,4 +8,27 @@ data class Messagebank(
var Voice_Type: String, var Voice_Type: String,
var Message_Detail: String, var Message_Detail: String,
var Message_TAGS: String var Message_TAGS: String
) ) {
/**
* Check if all fields are not empty and ANN_ID > 0
*/
fun isNotEmpty(): Boolean{
if (Description.isNotEmpty()){
if (Language.isNotEmpty()){
if (Voice_Type.isNotEmpty()){
if (Message_Detail.isNotEmpty()){
if (Message_TAGS.isNotEmpty()){
return true
}
}
}
}
}
return false
}
override fun toString(): String {
return "Messagebank(index=$index, Description='$Description', Language='$Language', ANN_ID=$ANN_ID, Voice_Type='$Voice_Type', Message_Detail='$Message_Detail', Message_TAGS='$Message_TAGS')"
}
}

View File

@@ -1,4 +1,13 @@
package database package database
@Suppress("unused") @Suppress("unused")
data class QueueFids(var index: UInt, var ALCODE: String, var FLNUM: String, var ORIGIN: String, var ETAD: String, var FREMARK: String) data class QueueFids(var index: UInt, var ALCODE: String, var FLNUM: String, var ORIGIN: String, var ETAD: String, var FREMARK: String){
fun isNotEmpty(): Boolean {
return ALCODE.isNotEmpty() && FLNUM.isNotEmpty() && ORIGIN.isNotEmpty() && ETAD.isNotEmpty() && FREMARK.isNotEmpty()
}
override fun toString(): String {
return "QueueFids(index=$index, ALCODE='$ALCODE', FLNUM='$FLNUM', ORIGIN='$ORIGIN', ETAD='$ETAD', FREMARK='$FREMARK')"
}
}

View File

@@ -1,6 +1,10 @@
package database package database
data class QueuePaging(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var BroadcastZones: String){ data class QueuePaging(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var BroadcastZones: String){
fun isNotEmpty(): Boolean {
return Date_Time.isNotEmpty() && Source.isNotEmpty() && Type.isNotEmpty() && Message.isNotEmpty() && BroadcastZones.isNotEmpty()
}
override fun toString(): String { override fun toString(): String {
return "QueuePaging(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', BroadcastZones='$BroadcastZones')" return "QueuePaging(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', BroadcastZones='$BroadcastZones')"
} }

View File

@@ -2,6 +2,12 @@ package database
data class QueueTable(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var SB_TAGS: String, var BroadcastZones: String, var Repeat: UInt, var Language: String){ data class QueueTable(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var SB_TAGS: String, var BroadcastZones: String, var Repeat: UInt, var Language: String){
/**
* Check if all fields are not empty
*/
fun isNotEmpty(): Boolean {
return Date_Time.isNotEmpty() && Source.isNotEmpty() && Type.isNotEmpty() && Message.isNotEmpty() && SB_TAGS.isNotEmpty() && BroadcastZones.isNotEmpty() && Language.isNotEmpty()
}
override fun toString(): String { override fun toString(): String {
return "QueueTable(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', SB_TAGS='$SB_TAGS', BroadcastZones='$BroadcastZones', Repeat=$Repeat, Language='$Language')" return "QueueTable(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', SB_TAGS='$SB_TAGS', BroadcastZones='$BroadcastZones', Repeat=$Repeat, Language='$Language')"
} }

View File

@@ -10,4 +10,13 @@ data class ScheduleBank(
var Repeat: UByte, var Repeat: UByte,
var Enable: Boolean, var Enable: Boolean,
var BroadcastZones: String, var BroadcastZones: String,
var Language: String) var Language: String) {
override fun toString(): String {
return "ScheduleBank(index=$index, Description='$Description', Day='$Day', Time='$Time', Soundpath='$Soundpath', Repeat=$Repeat, Enable=$Enable, BroadcastZones='$BroadcastZones', Language='$Language')"
}
fun isNotEmpty() : Boolean{
return Description.isNotEmpty() && Day.isNotEmpty() && Time.isNotEmpty() && Soundpath.isNotEmpty() && BroadcastZones.isNotEmpty() && Language.isNotEmpty()
}
}

View File

@@ -1,4 +1,17 @@
package database package database
@Suppress("unused") data class SoundChannel(val index: UInt, val channel: String, val ip: String) {
data class SoundChannel(val index: UInt, val channel: String, val ip: String)
override fun toString(): String {
return "SoundChannel(index=$index, channel='$channel', ip='$ip')"
}
/**
* Check if both channel and ip are not empty
* @return true if both channel and ip are not empty, false otherwise
*/
fun isNotEmpty() : Boolean {
return channel.isNotEmpty() && ip.isNotEmpty()
}
}

View File

@@ -1,5 +1,6 @@
package database package database
data class Soundbank( data class Soundbank(
var index: UInt, var index: UInt,
var Description: String, var Description: String,
@@ -8,4 +9,32 @@ data class Soundbank(
var Language: String, var Language: String,
var VoiceType: String, var VoiceType: String,
var Path: String, var Path: String,
) ){
/**
* Check if all fields are not empty
*/
fun isNotEmpty(): Boolean{
return Description.isNotEmpty() && TAG.isNotEmpty() && Category.isNotEmpty() && Language.isNotEmpty() && VoiceType.isNotEmpty() && Path.isNotEmpty()
}
/**
* Check if Category is valid
*/
fun ValidCategory() : Boolean{
return content.Category.entries.any{ it.name == this.Category}
}
/**
* Check if VoiceType is valid
*/
fun ValidVoiceType() : Boolean{
return content.VoiceType.entries.any{ it.name == this.VoiceType}
}
/**
* Check if Language is valid
*/
fun ValidLanguage() : Boolean{
return content.Language.entries.any{ it.name == this.Language}
}
}

View File

@@ -1,4 +1,31 @@
package database package database
@Suppress("unused") @Suppress("unused")
data class UserDB(var index: UInt, var username: String, var password: String, var location: String, var soundbank_tags: String, var messagebank_ann_id: String, var broadcastzones: String) data class UserDB(var index: UInt, var username: String, var password: String, var location: String, var airline_tags: String, var city_tags: String, var messagebank_ann_id: String, var broadcastzones: String){
override fun toString(): String {
return "UserDB(index=$index, username='$username', location='$location', airline_tags='$airline_tags', city_tags='$city_tags', messagebank_ann_id='$messagebank_ann_id', broadcastzones='$broadcastzones')"
}
fun isNotEmpty() : Boolean {
return username.isNotEmpty() && password.isNotEmpty() && location.isNotEmpty() && airline_tags.isNotEmpty() && city_tags.isNotEmpty() && messagebank_ann_id.isNotEmpty() && broadcastzones.isNotEmpty()
}
/**
* Compares this UserDB object with another UserDB object for equality.
* Two UserDB objects are considered equal if all their properties are equal, except for the index property.
*/
fun isEqual(other: UserDB?): Boolean {
if (other == null) return false
return this.username == other.username &&
this.password == other.password &&
this.location == other.location &&
this.airline_tags == other.airline_tags &&
this.city_tags == other.city_tags &&
this.messagebank_ann_id == other.messagebank_ann_id &&
this.broadcastzones == other.broadcastzones
}
}

View File

@@ -0,0 +1,2 @@
# gak mau lihat log javalin
org.slf4j.simpleLogger.defaultLogLevel=error

10
src/tinylog.properties Normal file
View File

@@ -0,0 +1,10 @@
writer1 = console
writer1.level = info
writer1.format = {date:dd/MM/yyyy HH:mm:ss} {class}.{method}(): {message}
writer2 = rolling file
writer2.format = {date:dd/MM/yyyy HH:mm:ss} {class}.{method}(): {message}
writer2.file = logs/{date:yyyy}/{date:MM}/{date:dd}.log
writer2.level = info
autoshutdown = false # optional, default: true

266
src/toa/Vx3K.kt Normal file
View File

@@ -0,0 +1,266 @@
package toa
import codes.Somecodes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.net.Inet4Address
import java.net.InetSocketAddress
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.function.BiConsumer
/**
* VX3K Protocol
* @param ipaddress IP address of the VX3K device, default to 192.168.14.1
* @param port Port number of the VX3K device, from 50050-50053 default to 50053
*/
class Vx3K(val ipaddress : String = "192.168.14.1", val port : Int = 50053) {
private val remotesocket : InetSocketAddress
init{
if (port !in 50050..50053){
throw IllegalArgumentException("Port number must be between 50050 and 50053")
}
try{
val inet = Inet4Address.getByName(ipaddress)
remotesocket = InetSocketAddress(inet, port)
} catch (_ : Exception){
throw IllegalArgumentException("Invalid IP address: $ipaddress")
}
}
/**
* Connect to the VX3K device
* @param timeout Connection timeout in milliseconds, default to 30000 ms
*/
fun Connect(timeout: Int = 30000){
try{
val socket = Socket()
// read timeout 5 seconds
socket.soTimeout = 5000
socket.connect(remotesocket, timeout)
} catch (e : Exception){
Logger.error { "Failed to connect with ${remotesocket.hostName}:${remotesocket.port}, Message: ${e.message}" }
}
}
/**
* Virtual Contact Input (Commmand 0x1001)
* @param ID : Device ID for VX3K, range 0 - 31
* @param CIN : Contact Input Number, range 0 - 15 for normal terminal, 16 for emergency contact input1, 17 for emergency contact input2
* @param isON : true for ON, false for OFF
* @param cb : Callback function with parameters (success: Boolean, message: String)
*/
fun VirtualCIN(ID: Short, CIN: Short, isON: Boolean, cb : BiConsumer<Boolean, String>){
val commandID = 0x1001.toShort()
if (ID !in 0..31){
cb.accept(false, "ID must be between 0 and 31")
return
}
if (CIN !in 0..17){
cb.accept(false, "CIN must be between 0 and 17")
return
}
val payload = ByteBuffer.allocate(6).order(ByteOrder.BIG_ENDIAN)
payload.putShort(ID)
payload.putShort(CIN)
payload.putShort(if (isON) 1 else 0)
val command = Make_Request_Command(commandID, payload.array())
Send_Receive(command,8){
success, reply ->
if (success){
val bb = ByteBuffer.wrap(reply).order(ByteOrder.BIG_ENDIAN)
val resp_commandID = bb.short
val resp_code = bb.short
if (resp_commandID==commandID){
if (resp_code.toInt() == 0){
cb.accept(true, "Virtual CIN command sent successfully to ${remotesocket.hostName}:${remotesocket.port}")
} else {
cb.accept(false, "Virtual CIN command failed with response code $resp_code from ${remotesocket.hostName}:${remotesocket.port}")
}
} else {
cb.accept(false, "Invalid response command ID $resp_commandID from ${remotesocket.hostName}:${remotesocket.port}")
}
} else {
cb.accept(false, "Failed to send Virtual CIN command to ${remotesocket.hostName}:${remotesocket.port}")
}
}
}
/**
* Open / Close Audio Input in Broadcast Pattern (Command 0x1003 for old firmware, 0x1101 for new firmware)
* @param NewFirmware Set to true if using new firmware version
* @param ID Device ID for VX3K, range 0 - 39 for new firmware, 0 - 31 for old firmware
* @param Channel Audio Input Channel, range 0 - 3
* @param BroadcastZones Vx3K_BroadcastZone object with selected broadcast zones
* @param cb Callback function with parameters (success: Boolean, message: String)
*/
fun AudioInput_BroadcastPattern(NewFirmware : Boolean, ID: Short, Channel: Short, BroadcastZones: Vx3K_BroadcastZone, cb: BiConsumer<Boolean, String>){
val CommandID = if (NewFirmware) 0x1101.toShort() else 0x1003.toShort()
if (NewFirmware){
if (ID !in 0..39){
cb.accept(false, "ID must be between 0 and 39")
return
}
} else {
if (ID !in 0..31){
cb.accept(false, "ID must be between 0 and 31")
return
}
}
if (Channel !in 0..3){
cb.accept(false, "Channel must be between 0 and 3")
return
}
val payload = ByteBuffer.allocate(if (NewFirmware) 86 else 70).order(ByteOrder.BIG_ENDIAN)
// Audio Input = 1
payload.putShort(1)
// VX3K ID
payload.putShort(ID)
// Audio Input Channel
payload.putShort(Channel)
// Broadcast Pattern Zones
payload.put(BroadcastZones.payload)
val command = Make_Request_Command(CommandID, payload.array())
Send_Receive(command,8){
success, reply ->
if (success){
val bb = ByteBuffer.wrap(reply).order(ByteOrder.BIG_ENDIAN)
val resp_commandID = bb.short
val resp_code = bb.short
if (resp_commandID==CommandID){
if (resp_code.toInt() == 0){
cb.accept(true, "Open Audio Input Broadcast command sent successfully to ${remotesocket.hostName}:${remotesocket.port}")
} else {
cb.accept(false, "Open Audio Input Broadcast command failed with response code $resp_code from ${remotesocket.hostName}:${remotesocket.port}")
}
} else {
cb.accept(false, "Invalid response command ID $resp_commandID from ${remotesocket.hostName}:${remotesocket.port}")
}
} else {
cb.accept(false, "Failed to send Open Audio Input Broadcast command to ${remotesocket.hostName}:${remotesocket.port}")
}
}
}
/**
* Open / Close Network Broadcast Pattern (Command 0x1102 for old firmware, 0x1104 for new firmware)
* @param NewFirmware Set to true if using new firmware version
* @param multicastIP Multicast IP address in string format, from 224.0.0.0 ~ 239.255.255.255, default to 224.0.0.1
* @param port UDP Port number, port 5000-5255 is invalid, default to 5300
* @param priority Broadcast Priority value, range 1 - 1024, default to 512 (to be checked later)
* @param isBGM true for BGM, false for Paging
* @param SSRC SSRC value, range 1 - 65535, default to 1 (to be checked later)
* @param payloadType RTP Payload Type, range 0 - 127, default to 0 (to be checked later)
* @param payloadSize RTP Payload Size, range 0 - 1500, default to 1000 (to be checked later)
* @param BroadcastZones Vx3K_BroadcastZone object with selected broadcast zones
* @param cb Callback function with parameters (success: Boolean, message: String)
*/
fun Network_BroadcastPattern(NewFirmware: Boolean,multicastIP: String = "224.0.0.1", port: Int = 5300, priority: Int = 512, isBGM: Boolean, SSRC: UShort = 1u, payloadType: Short = 0, payloadSize: Short = 1000, BroadcastZones: Vx3K_BroadcastZone, cb:BiConsumer<Boolean, String>){
val CommandID = if (NewFirmware) 0x1104.toShort() else 0x1102.toShort()
if (Somecodes.ValidIPV4(multicastIP)){
val inet = Inet4Address.getByName(multicastIP)
if (inet.isMulticastAddress){
if (port in 5256..65535){
if (priority in 1..1024){
if (SSRC in 1u..65535u){
if (payloadType in 0..127){
if (payloadSize in 0..1500){
val jitterbuffer = 1000 //TODO 32 signed integer, to be checked later
val payload = ByteBuffer.allocate(if (NewFirmware) 100 else 84)
payload.put(inet.address)
payload.putShort(port.toShort())
payload.putShort(priority.toShort())
payload.putShort(if (isBGM) 1 else 0)
payload.putShort(SSRC.toShort())
payload.putShort(payloadType)
payload.putShort(payloadSize)
payload.putInt(jitterbuffer)
payload.put(BroadcastZones.payload)
val command = Make_Request_Command(CommandID, payload.array())
Send_Receive(command,10){
success, reply ->
if (success){
val bb = ByteBuffer.wrap(reply).order(ByteOrder.BIG_ENDIAN)
val resp_commandID = bb.short
val resp_code = bb.short
// buang 2 short
bb.short
bb.short
val sound_source_number = bb.short
if (resp_commandID==CommandID){
if (resp_code.toInt() == 0){
cb.accept(true, "Open Network Broadcast command sent successfully to ${remotesocket.hostName}:${remotesocket.port}, Sound Source Number: $sound_source_number")
} else {
cb.accept(false, "Open Network Broadcast command failed with response code $resp_code from ${remotesocket.hostName}:${remotesocket.port}")
}
} else {
cb.accept(false, "Invalid response command ID $resp_commandID from ${remotesocket.hostName}:${remotesocket.port}")
}
} else {
cb.accept(false, "Failed to send Open Network Broadcast command to ${remotesocket.hostName}:${remotesocket.port}")
}
}
} else cb.accept(false, "Payload Size must be between 0 and 1500")
} else cb.accept(false, "Payload Type must be between 0 and 127")
} else cb.accept(false, "SSRC must be between 1 and 65535")
} else cb.accept(false, "Priority must be between 1 and 1024")
} else cb.accept(false, "Port must be greater than 5255 and less than 65536")
} else cb.accept(false, "multicastIP must between 224.0.0.0 ~ 239.255.255.255")
} else cb.accept(false, "Multicast IP address invalid")
}
/**
* Send command and receive reply from VX3K device
* @param command Command byte array to send
* @param expectedlength Expected length of the reply byte array
* @param cb Callback function with parameters (success: Boolean, reply: ByteArray)
*/
private fun Send_Receive(command: ByteArray, expectedlength: Int, cb : BiConsumer<Boolean, ByteArray>){
CoroutineScope(Dispatchers.IO).launch {
try{
val tcp = Socket()
tcp.soTimeout = 5000
tcp.connect(remotesocket, 30000)
val outstream = tcp.getOutputStream()
val instream = tcp.getInputStream()
outstream.write(command)
outstream.flush()
val reply = ByteArray(expectedlength)
instream.read(reply)
outstream.close()
instream.close()
tcp.close()
cb.accept(true, reply)
} catch (_: Exception){
cb.accept(false, ByteArray(0))
}
}
}
/**
* Wrap payload into VX3K Request Command format
* @param commandID Command ID
* @param Payload Payload data
* @return ByteArray of the complete command
*/
private fun Make_Request_Command(commandID: Short, Payload: ByteArray) : ByteArray {
val result = ByteBuffer.allocate(8+Payload.size).order(ByteOrder.BIG_ENDIAN)
// command ID (2 bytes)
result.putShort(commandID)
// Response code (2 bytes), set 0 for Request
result.putShort(0)
// command length = command header (8 bytes) + payload size
result.putShort((Payload.size+8).toShort())
// flag and reserver = 0 (2 bytes)
result.putShort(0)
result.put(Payload)
// read to byte array
return result.array()
}
}

View File

@@ -0,0 +1,67 @@
package toa
import kotlin.experimental.and
import kotlin.experimental.or
/**
* VX3K Broadcast Zone Configuration
* Old Firmware: Broadcast Zone 1 - 512
* New Firmware: Broadcast Zone 1 - 640
* @param NewFirmware Set to true if using new firmware version
*/
@Suppress("unused")
class Vx3K_BroadcastZone(val NewFirmware: Boolean = false) {
val payload: ByteArray = if (NewFirmware) ByteArray(80) else ByteArray(64)
/**
* Set a zone as active
* @param zonenumber Zone number to set (1 to 512 for old firmware, 1 to 640 for new firmware)
*/
fun SetZone(zonenumber: Int){
if (zonenumber<1) throw Exception("Minimum zone number is 1")
if (NewFirmware){
if (zonenumber>640) throw Exception("Maximum zone number is 640 for new firmware")
} else {
if (zonenumber>512) throw Exception("Maximum zone number is 512 for old firmware")
}
val byteIndex = (zonenumber - 1) / 8
val bitIndex = (zonenumber - 1) % 8
payload[byteIndex] = payload[byteIndex] or (1 shl bitIndex).toByte()
}
/**
* Clear a zone (set as inactive)
* @param zonenumber Zone number to clear (1 to 512 for old firmware, 1 to 640 for new firmware)
*/
fun ClearZone(zonenumber: Int){
if (zonenumber<1) throw Exception("Minimum zone number is 1")
if (NewFirmware){
if (zonenumber>640) throw Exception("Maximum zone number is 640 for new firmware")
} else {
if (zonenumber>512) throw Exception("Maximum zone number is 512 for old firmware")
}
val byteIndex = (zonenumber - 1) / 8
val bitIndex = (zonenumber - 1) % 8
payload[byteIndex] = payload[byteIndex] and ((1 shl bitIndex).inv().toByte())
}
/**
* Set all zones as active
*/
fun SetAllZones(){
for (i in payload.indices){
payload[i] = 0xFF.toByte()
}
}
/**
* Clear all zones (set all as inactive)
*/
fun ClearAllZones(){
for (i in payload.indices){
payload[i] = 0
}
}
}

View File

@@ -0,0 +1,4 @@
package web
class KeyValueMessage(val key: String, val value: String) {
}

View File

@@ -0,0 +1,9 @@
package web
class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, var isPlaying: Boolean) {
companion object{
fun fromBarixConnection(bc: barix.BarixConnection): StreamerOutputData {
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying())
}
}
}

View File

@@ -2,6 +2,8 @@ package web
import StreamerOutputs import StreamerOutputs
import codes.Somecodes import codes.Somecodes
import codes.Somecodes.Companion.GetSensorsInfo
import codes.Somecodes.Companion.GetUptime
import codes.Somecodes.Companion.ListAudioFiles import codes.Somecodes.Companion.ListAudioFiles
import codes.Somecodes.Companion.ValiDateForLogHtml import codes.Somecodes.Companion.ValiDateForLogHtml
import codes.Somecodes.Companion.ValidFile import codes.Somecodes.Companion.ValidFile
@@ -9,6 +11,7 @@ import codes.Somecodes.Companion.ValidIPV4
import codes.Somecodes.Companion.ValidScheduleDay import codes.Somecodes.Companion.ValidScheduleDay
import codes.Somecodes.Companion.ValidScheduleTime import codes.Somecodes.Companion.ValidScheduleTime
import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.ValidStrings
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import content.Category import content.Category
@@ -19,8 +22,10 @@ import database.BroadcastZones
import database.LanguageLink import database.LanguageLink
import database.MariaDB import database.MariaDB
import database.Messagebank import database.Messagebank
import database.ScheduleBank
import database.SoundChannel import database.SoundChannel
import database.Soundbank import database.Soundbank
import database.UserDB
import db import db
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.before import io.javalin.apibuilder.ApiBuilder.before
@@ -34,6 +39,7 @@ import io.javalin.http.Context
import io.javalin.json.JavalinJackson import io.javalin.json.JavalinJackson
import io.javalin.websocket.WsMessageContext import io.javalin.websocket.WsMessageContext
import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.nio.file.Files
import java.time.LocalDateTime import java.time.LocalDateTime
@Suppress("unused") @Suppress("unused")
@@ -52,6 +58,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
fun Start() { fun Start() {
app = Javalin.create { config -> app = Javalin.create { config ->
config.useVirtualThreads = true config.useVirtualThreads = true
config.staticFiles.add("/webpage") config.staticFiles.add("/webpage")
@@ -82,7 +89,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
// Set user session // Set user session
it.sessionAttribute("user", user.first) it.sessionAttribute("user", user.first)
println("User ${user.first} logged in") //println("User ${user.first} logged in")
// Redirect to home page // Redirect to home page
it.redirect("home.html") it.redirect("home.html")
} }
@@ -92,26 +99,30 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
before { CheckUsers(it) } before { CheckUsers(it) }
ws("/ws") { ws -> ws("/ws") { ws ->
// WebSocket endpoint for home // WebSocket endpoint for home
ws.onClose { wsCloseContext -> //
// TODO Handle WebSocket close event
println("WebSocket closed: ${wsCloseContext.session.remoteAddress}")
}
ws.onMessage { wsMessageContext -> ws.onMessage { wsMessageContext ->
try { try {
val cmd = val cmd =
objectmapper.readValue(wsMessageContext.message(), WebsocketCommand::class.java) objectmapper.readValue(wsMessageContext.message(), WebsocketCommand::class.java)
when (cmd.command) { when (cmd.command) {
"getSystemTime" -> { "getSystemTime" -> {
val systemtime = LocalDateTime.now().format(Somecodes.datetimeformat1)
val uptime = GetUptime()
SendReply( SendReply(
wsMessageContext, wsMessageContext,
cmd.command, cmd.command,
LocalDateTime.now().format(Somecodes.datetimeformat1) if (uptime.isNotEmpty()) "Date & Time : $systemtime\nSystem Uptime : $uptime" else "Date & Time : $systemtime"
) )
} }
"getCPUStatus" -> { "getCPUStatus" -> {
Somecodes.getCPUUsage { vv -> Somecodes.getCPUUsage { vv ->
SendReply(wsMessageContext, cmd.command, vv) val sv = GetSensorsInfo()
if (sv.isNotEmpty()){
SendReply(wsMessageContext, cmd.command, vv+"\n"+sv)
} else {
SendReply(wsMessageContext, cmd.command, vv)
}
} }
} }
@@ -124,8 +135,9 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
"getNetworkStatus" -> { "getNetworkStatus" -> {
// TODO Get Network status Somecodes.GetNetworkStatus { nn ->
SendReply(wsMessageContext, cmd.command, "OK") SendReply(wsMessageContext, cmd.command, objectmapper.writeValueAsString(nn))
}
} }
"getPagingQueue" ->{ "getPagingQueue" ->{
@@ -137,7 +149,8 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
"getStreamerOutputs" -> { "getStreamerOutputs" -> {
SendReply(wsMessageContext, cmd.command, objectmapper.writeValueAsString(StreamerOutputs.values.toList())) val reply : List<StreamerOutputData> = StreamerOutputs.map { so -> StreamerOutputData.fromBarixConnection(so.value) }
SendReply(wsMessageContext, cmd.command, objectmapper.writeValueAsString(reply))
} }
else -> { else -> {
@@ -149,10 +162,9 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
} }
ws.onConnect { wsConnectContext -> // ws.onConnect { wsConnectContext ->
// TODO Handle WebSocket connect event // println("WebSocket connected: ${wsConnectContext.session.remoteAddress}")
println("WebSocket connected: ${wsConnectContext.session.remoteAddress}") // }
}
} }
} }
@@ -195,43 +207,99 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
it.result(objectmapper.writeValueAsString(ScheduleDay.entries.map { day -> day.toString() })) it.result(objectmapper.writeValueAsString(ScheduleDay.entries.map { day -> day.toString() }))
} }
} }
path("ListFiles/{Language}/{VoiceType}/{Category}") {
get { ctx ->
val language = ctx.pathParam("Language")
val voiceType = ctx.pathParam("VoiceType")
val category = ctx.pathParam("Category")
if (ValidString(language) && Language.entries.any { lang -> lang.name == language }) {
if (ValidString(voiceType) && VoiceType.entries.any { vt -> vt.name == voiceType }) {
if (ValidString(category) && Category.entries.any { cat -> cat.name == category }) {
val dir = Somecodes.SoundbankDirectory(
Language.valueOf(language),
VoiceType.valueOf(voiceType),
Category.valueOf(category)
)
if (Files.isDirectory(dir)){
val list = ListAudioFiles(dir)
ctx.result(objectmapper.writeValueAsString(list))
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Directory does not exist")))
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid Category")))
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid VoiceType")))
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid Language")))
}
}
path("SoundBank") { path("SoundBank") {
get("List") { get("List") {
it.result(MariaDB.ArrayListtoString(db.soundDB.List)) it.result(MariaDB.ArrayListtoString(db.soundDB.List))
} }
get("ListFiles"){ get("ListFiles") {
it.result(objectmapper.writeValueAsString(ListAudioFiles("C:\\soundbank"))) it.result(objectmapper.writeValueAsString(ListAudioFiles(Somecodes.Soundbank_directory)))
}
get("GetPhrases/{Language}/{VoiceType}"){
val language = it.pathParam("Language")
val voiceType = it.pathParam("VoiceType")
if (ValidString(language) && Language.entries.any { lang -> lang.name == language }) {
if (ValidString(voiceType) && VoiceType.entries.any { vt -> vt.name == voiceType }) {
val phrases = db.soundDB.List
.filter { sb -> sb.Language == language }
.filter {sb -> sb.VoiceType == voiceType}
.filter { sb -> sb.Category == Category.Phrase.name}
.distinctBy { sb -> sb.TAG }
.sortedBy { sb -> sb.TAG }
it.result(objectmapper.writeValueAsString(phrases))
} else it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid VoiceType")))
} else it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid Language")))
}
get("AirlineTags") { ctx ->
val value = db.soundDB.List
.filter { it.Category == Category.Airplane_Name.name }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
.map { KeyValueMessage(it.TAG, it.Description) }
ctx.result(objectmapper.writeValueAsString(value))
}
get("CityTags") { ctx ->
val value = db.soundDB.List
.filter { it.Category == Category.City.name }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
.map { KeyValueMessage(it.TAG, it.Description) }
ctx.result(objectmapper.writeValueAsString(value))
} }
post("Add") { post("Add") {
try { try {
val addvalue = objectmapper.readValue(it.body(), Soundbank::class.java) val addvalue = objectmapper.readValue(it.body(), Soundbank::class.java)
if (ValidString(addvalue.Description)) { if (addvalue.isNotEmpty()){
if (ValidString(addvalue.TAG)) { // check apakah TAG sudah ada untuk language, category dan voicetype yang sama
if (ValidString(addvalue.Category)) { val exists = db.soundDB.List.any { sb ->
if (ValidString(addvalue.Language)) { sb.TAG == addvalue.TAG && sb.Language == addvalue.Language && sb.Category == addvalue.Category && sb.VoiceType == addvalue.VoiceType
if (ValidString(addvalue.Path)) { }
// check apakah TAG sudah ada untuk language dan category yang sama if (!exists) {
val exists = db.soundDB.List.any { sb -> val absolutepath = Somecodes.SoundbankDirectory(Language.valueOf(addvalue.Language), VoiceType.valueOf(addvalue.VoiceType), Category.valueOf(addvalue.Category)).resolve(addvalue.Path)
sb.TAG == addvalue.TAG && sb.Language == addvalue.Language && sb.Category == addvalue.Category if (Files.isRegularFile(absolutepath)) {
} addvalue.Path = absolutepath.toAbsolutePath().toString()
if (!exists) { if (db.soundDB.Add(addvalue)) {
if (ValidFile(addvalue.Path)) { db.soundDB.Resort()
if (db.soundDB.Add(addvalue)) { it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.soundDB.Resort() db.Add_Log("AAS", "Added Sound Bank: $addvalue")
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) } else it.status(500)
} else it.status(500) .result(objectmapper.writeValueAsString(resultMessage("Failed to add soundbank to database")))
.result(objectmapper.writeValueAsString(resultMessage("Failed to add soundbank to database"))) } else it.status(400)
} else it.status(400) .result(objectmapper.writeValueAsString(resultMessage("Invalid Path, file does not exist")))
.result(objectmapper.writeValueAsString(resultMessage("Invalid Path, file does not exist"))) } else it.status(400)
} else it.status(400) .result(objectmapper.writeValueAsString(resultMessage("TAG=${addvalue.TAG} already exists for the same Language=${addvalue.Language} and Category=${addvalue.Category}")))
.result(objectmapper.writeValueAsString(resultMessage("TAG=${addvalue.TAG} already exists for the same Language=${addvalue.Language} and Category=${addvalue.Category}"))) } else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Some fields are empty")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Path")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Language"))) } catch (e: Exception) {
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Category"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid request body, Message: ${e.message}")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid TAG")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Description")))
} catch (_: Exception) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid request body")))
} }
} }
delete("List") { delete("List") {
@@ -239,6 +307,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.soundDB.Clear()) { if (db.soundDB.Clear()) {
db.soundDB.Get() db.soundDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Sound Bank table")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate soundbank table"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate soundbank table")))
} }
@@ -252,6 +321,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.soundDB.DeleteByIndex(index.toInt())) { if (db.soundDB.DeleteByIndex(index.toInt())) {
db.soundDB.Resort() db.soundDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Sound Bank with index $index")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete soundbank with index $index"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete soundbank with index $index")))
} }
@@ -260,68 +330,79 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
patch("UpdateByIndex/{index}") { patch("UpdateByIndex/{index}") {
// update by index // update by index
val index = it.pathParam("index").toUIntOrNull() val index = it.pathParam("index").toUIntOrNull()
if (index == null) { if (index!=null){
// tidak ada path param index
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
} else {
val sb = db.soundDB.List.find { xx -> xx.index == index } val sb = db.soundDB.List.find { xx -> xx.index == index }
if (sb == null) { if (sb!=null){
// soundbank dengan index tersebut tidak ditemukan try{
it.status(404).result(objectmapper.writeValueAsString(resultMessage("Soundbank with index $index not found"))) //println("index=$index, body=${it.body()}")
} else { val newsb = objectmapper.readValue(it.body(), Soundbank::class.java)
// soundbank dengan index tersebut ditemukan, sekarang update //println("newsb=$newsb")
val json: JsonNode = objectmapper.readTree(it.body()) if (newsb.isNotEmpty()){
if (json.isEmpty) { var varchanged = false
it.status(400).result(objectmapper.writeValueAsString(resultMessage("UpdateByIndex with index=$index has empty body"))) if (newsb.Description != sb.Description) {
} else { sb.Description = newsb.Description
val _description = json.get("Description").asText("") varchanged = true
val _tag = json.get("TAG").asText("") }
val _category = json.get("Category").asText("") if (newsb.TAG != sb.TAG) {
val _language = json.get("Language").asText("") sb.TAG = newsb.TAG
val _path = json.get("Path").asText("") varchanged = true
var changed = false }
if (ValidString(_description) && _description != sb.Description) { if (newsb.ValidCategory()) {
sb.Description = _description if (newsb.Category != sb.Category){
changed = true sb.Category = newsb.Category
} varchanged = true
if (ValidString(_tag) && _tag != sb.TAG) { }
sb.TAG = _tag
changed = true
}
if (ValidString(_category) && _category != sb.Category) {
if (Category.entries.any { cat ->
cat.name == _category
}) {
sb.Category = _category
changed = true
} else { } else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Category"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Category")))
return@patch return@patch
} }
} if (newsb.ValidLanguage()) {
if (ValidString(_language) && _language != sb.Language) { if (newsb.Language != sb.Language){
sb.Language = _language sb.Language = newsb.Language
changed = true varchanged = true
} }
if (ValidString(_path) && _path != sb.Path) {
if (ValidFile(_path)) {
sb.Path = _path
changed = true
} else { } else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Path, file does not exist"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Language")))
return@patch return@patch
} }
} if (newsb.ValidVoiceType()) {
if (changed) { if (newsb.VoiceType != sb.VoiceType){
if (db.soundDB.UpdateByIndex(index.toInt(), sb)) { sb.VoiceType = newsb.VoiceType
db.soundDB.Resort() varchanged = true
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) }
} else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to update soundbank with index $index"))) } else {
} else it.status(400) it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid VoiceType")))
.result(objectmapper.writeValueAsString(resultMessage("Nothing has changed for soundbank with index $index"))) return@patch
}
val newpath = Somecodes.SoundbankDirectory(newsb.Language, newsb.VoiceType, newsb.Category).resolve(newsb.Path)
if (Files.isRegularFile(newpath)){
if (newpath.toAbsolutePath().toString() != sb.Path) {
sb.Path = newpath.toAbsolutePath().toString()
varchanged = true
}
} else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid path")))
return@patch
}
if (varchanged) {
//println("Some fields changed for index=$index, updating...")
if (db.soundDB.UpdateByIndex(index.toInt(), sb)) {
db.soundDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Soundbank $sb")
} else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to update soundbank with index $index")))
} else it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Nothing has changed for soundbank with index $index")))
} else Exception("Some fields are empty")
} catch(e: Exception){
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Unable to parse request body to Soundbank, Message: ${e.message}")))
} }
}
} } else it.status(404).result(objectmapper.writeValueAsString(resultMessage("Soundbank with index $index not found")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
} }
get("ExportXLSX") { get("ExportXLSX") {
val xlsxdata = db.soundDB.Export_XLSX() val xlsxdata = db.soundDB.Export_XLSX()
@@ -362,8 +443,15 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
// get messagebank list // get messagebank list
it.result(MariaDB.ArrayListtoString(db.messageDB.List)) it.result(MariaDB.ArrayListtoString(db.messageDB.List))
} }
post("Add"){ get("MessageIDs") { ctx ->
val json : JsonNode = objectmapper.readTree(it.body()) val value = db.messageDB.List
.distinctBy { it.ANN_ID }
.sortedBy { it.ANN_ID }
.map { KeyValueMessage(it.ANN_ID.toString(), it.Description) }
ctx.result(objectmapper.writeValueAsString(value))
}
post("Add") {
val json: JsonNode = objectmapper.readTree(it.body())
val description = json.get("Description")?.asText("") ?: "" val description = json.get("Description")?.asText("") ?: ""
val language = json.get("Language")?.asText("") ?: "" val language = json.get("Language")?.asText("") ?: ""
val ann_id = json.get("ANN_ID")?.asInt()?.toUInt() ?: 0u val ann_id = json.get("ANN_ID")?.asInt()?.toUInt() ?: 0u
@@ -380,6 +468,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.Add(mb)){ if (db.messageDB.Add(mb)){
db.messageDB.Resort() db.messageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Added Messagebank: $mb")
} else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add messagebank to database"))) } else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add messagebank to database")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Message_TAGS"))) } else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Message_TAGS")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Message_Detail"))) } else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Message_Detail")))
@@ -393,6 +482,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.Clear()) { if (db.messageDB.Clear()) {
db.messageDB.Get() db.messageDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Message Bank table")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate messagebank table"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate messagebank table")))
} }
@@ -406,6 +496,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.DeleteByIndex(index.toInt())) { if (db.messageDB.DeleteByIndex(index.toInt())) {
db.messageDB.Resort() db.messageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Message Bank with index $index")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete messagebank with index $index"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete messagebank with index $index")))
} }
@@ -466,6 +557,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.UpdateByIndex(index.toInt(), mb)) { if (db.messageDB.UpdateByIndex(index.toInt(), mb)) {
db.messageDB.Resort() db.messageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Messagebank $mb")
} else it.status(500) } else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update messagebank with index $index"))) .result(objectmapper.writeValueAsString(resultMessage("Failed to update messagebank with index $index")))
} else it.status(400) } else it.status(400)
@@ -518,7 +610,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
val json: JsonNode = objectmapper.readTree(it.body()) val json: JsonNode = objectmapper.readTree(it.body())
val tag = json.get("tag").asText("") val tag = json.get("tag").asText("")
val languages = json.get("language").asText("").split(";") val languages = json.get("language").asText("").split(";")
println("Add Language Link, tag=$tag, languages=$languages") //println("Add Language Link, tag=$tag, languages=$languages")
if (ValidString(tag)){ if (ValidString(tag)){
if (languages.all { xx -> Language.entries.any { yy -> yy.name.equals(xx,true)} }){ if (languages.all { xx -> Language.entries.any { yy -> yy.name.equals(xx,true)} }){
if (!db.languageDB.List.any { ll -> ll.TAG.equals(tag,true) }) { if (!db.languageDB.List.any { ll -> ll.TAG.equals(tag,true) }) {
@@ -526,13 +618,14 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.Add(newvalue)){ if (db.languageDB.Add(newvalue)){
db.languageDB.Resort() db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Added Language Link: $newvalue")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add language link to database"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add language link to database")))
println("Failed to add language link to database") //println("Failed to add language link to database")
} }
} else { } else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("TAG=$tag already exists"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("TAG=$tag already exists")))
println("TAG=$tag already exists") //println("TAG=$tag already exists")
} }
} else { } else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Contains unsupported language"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("Contains unsupported language")))
@@ -548,6 +641,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.Clear()) { if (db.languageDB.Clear()) {
db.languageDB.Resort() db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Language Link table")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate language link table"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate language link table")))
} }
@@ -561,6 +655,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.DeleteByIndex(index.toInt())) { if (db.languageDB.DeleteByIndex(index.toInt())) {
db.languageDB.Resort() db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Language Link with index $index")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete language link with index $index"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete language link with index $index")))
} }
@@ -595,6 +690,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.UpdateByIndex(index.toInt(), ll)) { if (db.languageDB.UpdateByIndex(index.toInt(), ll)) {
db.languageDB.Resort() db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Language Link $ll")
} else it.status(500) } else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update language link with index $index"))) .result(objectmapper.writeValueAsString(resultMessage("Failed to update language link with index $index")))
} else it.status(400) } else it.status(400)
@@ -647,12 +743,56 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.scheduleDB.Clear()) { if (db.scheduleDB.Clear()) {
db.scheduleDB.Get() db.scheduleDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Schedule Bank table")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate schedulebank table"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate schedulebank table")))
} }
} }
post("Add"){ post("Add"){
// TODO add new schedule // TODO add new schedule
// recheck lagi tambahan steph
val json: JsonNode = objectmapper.readTree(it.body())
val description = json.get("Description")?.asText("") ?: ""
val day = json.get("Day")?.asText("") ?: ""
val time = json.get("Time")?.asText("") ?: ""
val soundpath = json.get("Soundpath")?.asText("") ?: ""
val repeat = json.get("Repeat")?.asInt()?.toUByte() ?: 0u
val enable = json.get("Enable")?.asBoolean() ?: false
val broadcast_zones = json.get("BroadcastZones")?.asText("") ?: ""
val language = json.get("Language")?.asText("") ?: ""
if (ValidString(description)){
if (ValidString(day) && ValidScheduleDay(day)){
if (ValidString(time) && ValidScheduleTime(time)){
if (ValidString(soundpath) && ValidFile(soundpath)){
if (repeat in 0u..127u){
if (ValidString(broadcast_zones)){
val zones = broadcast_zones.split(";")
if (zones.all { zz -> db.broadcastDB.List.any { xx -> xx.description.equals(zz,true) } }){
if (ValidString(language) && Language.entries.any{ lang -> lang.name == language }){
val newvalue = ScheduleBank(
0u,
description,
day,
time,
soundpath,
repeat,
enable,
broadcast_zones,
language
)
if (db.scheduleDB.Add(newvalue)){
db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Added Schedule Bank: $newvalue")
} else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add schedule to database")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Language")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Contains unsupported BroadcastZones")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid BroadcastZones")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Repeat, must be between 0-127")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Soundpath")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Time format, must be HH:mm")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Day format")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Description")))
} }
delete("DeleteByIndex/{index}") { delete("DeleteByIndex/{index}") {
// delete by index // delete by index
@@ -663,6 +803,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.scheduleDB.DeleteByIndex(index.toInt())) { if (db.scheduleDB.DeleteByIndex(index.toInt())) {
db.scheduleDB.Resort() db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Schedule Bank with index $index")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete schedule with index $index"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete schedule with index $index")))
} }
@@ -742,6 +883,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.scheduleDB.UpdateByIndex(index.toInt(), sb)) { if (db.scheduleDB.UpdateByIndex(index.toInt(), sb)) {
db.scheduleDB.Resort() db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Schedule : $sb")
} else it.status(500) } else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update schedule with index $index"))) .result(objectmapper.writeValueAsString(resultMessage("Failed to update schedule with index $index")))
} else it.status(400) } else it.status(400)
@@ -777,12 +919,204 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
db.scheduleDB.Resort() db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to import schedulebank from XLSX"))) it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to import schedulebank from XLSX")))
}
} catch (e: Exception) {
it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid XLSX file")))
}
}
}
path("UserManagement") {
get("List") {
it.result(objectmapper.writeValueAsString(db.userDB.List))
}
delete("List") {
if (db.userDB.Clear()) {
db.userDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear User Management table")
} else {
it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to truncate user table")))
}
}
post("Add") { ctx ->
val json: JsonNode = objectmapper.readTree(ctx.body())
val username = json.get("username").asText("")
val password = json.get("password").asText("")
val location = json.get("location").asText("")
val airline_tags = json.get("airline_tags").asText("")
val city_tags = json.get("city_tags").asText("")
val messagebank_ann_id = json.get("messagebank_ann_id").asText("")
val broadcastzones = json.get("broadcastzones").asText("")
if (ValidStrings(listOf(username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones))) {
if (!db.Username_exists(username)) {
// check apakah ada airline tag yang tidak ada di soundbank
val atags = airline_tags.split(";").map { it.trim() }
.filter { it.isNotEmpty() }.distinct()
val airlinetags = db.Get_AirlineCode_Tags()
if (atags.all { tag -> airlinetags.any { it == tag } }) {
// check apakah ada city tag yang tidak ada di soundbank
val ctags = city_tags.split(";").map { it.trim() }
.filter { it.isNotEmpty() }.distinct()
val citytags = db.Get_City_Tags()
if (ctags.all { tag -> citytags.any { it == tag } }) {
val bzdesc =
broadcastzones.split(";").map { it.trim() }
.filter { it.isNotEmpty() }.distinct()
val bzlist = db.Get_BroadcastZone_List()
if (bzdesc.all { desc -> bzlist.any { it == desc } }) {
val mbids = messagebank_ann_id.split(";")
.map { it.trim() }
.filter { it.isNotEmpty() }.distinct()
.mapNotNull { it.toUIntOrNull() }
val mbankids = db.Get_MessageID_List()
if (mbids.all { id -> mbankids.any { it == id } }) {
// semua valid, tambain ke database
val newuser = UserDB(
0u,
username,
password,
location,
airline_tags,
city_tags,
messagebank_ann_id,
broadcastzones
)
if (db.userDB.Add(newuser)) {
db.userDB.Resort()
ctx.result(objectmapper.writeValueAsString(resultMessage("OK") ))
db.Add_Log("AAS", "Added User: $newuser")
} else ctx.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add user to database")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some ANN_ID not found in Messagebank") ))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some broadcast zone tags not found in soundbank")) )
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some city tags not found in soundbank")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some airline tags not found in soundbank")))
} else ctx.status(400).result(objectmapper.writeValueAsString("Username already exists"))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Not all fields have value")))
}
delete("DeleteByIndex/{index}") {
// delete by index
val index = it.pathParam("index").toUIntOrNull()
if (index == null) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
} else {
if (db.userDB.DeleteByIndex(index.toInt())) {
db.userDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted User with index $index")
} else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete user with index $index")))
}
}
patch("UpdateByIndex/{index}"){ ctx ->
// update by index
val index = ctx.pathParam("index").toUIntOrNull()
if (index!=null){
val user = db.userDB.List.find { xx -> xx.index == index }
if (user!=null){
val json : JsonNode = objectmapper.readTree(ctx.body())
if (!json.isEmpty){
val _username = json.get("username").asText("")
val _password = json.get("password").asText("")
val _location = json.get("location").asText("")
val _airline_tags = json.get("airline_tags").asText("")
val _city_tags = json.get("city_tags").asText("")
val _messagebank_ann_id = json.get("messagebank_ann_id").asText("")
val _broadcastzones = json.get("broadcastzones").asText("")
if (ValidStrings(listOf(_username, _password, _location, _airline_tags, _city_tags, _messagebank_ann_id, _broadcastzones))) {
val _otherusername = db.userDB.List.find { xx -> xx.username.equals(_username, true) && xx.index != index }
if (_otherusername == null) {
// belum ada user lain yang pakai username ini
val atags = _airline_tags.split(";").map { it.trim() }.filter { it.isNotEmpty() }.distinct()
val ctags = _city_tags.split(";").map { it.trim() }.filter { it.isNotEmpty() }.distinct()
val msgids = _messagebank_ann_id.split(";").map { it.trim() }.filter { it.isNotEmpty() }.distinct().mapNotNull { it.toUIntOrNull() }
val bzdesc = _broadcastzones.split(";").map { it.trim() }.filter { it.isNotEmpty() }.distinct()
val airline_tags = db.Get_AirlineCode_Tags()
val city_tags = db.Get_City_Tags()
val msgbankids = db.Get_MessageID_List()
val bzlist = db.Get_BroadcastZone_List()
if (atags.all { tag -> airline_tags.any { it == tag } }) {
if (ctags.all { tag -> city_tags.any { it == tag } }) {
if (msgids.all { tag -> msgbankids.any { it == tag } }) {
if (bzdesc.all { tag -> bzlist.any { it == tag } }) {
val editeduser = UserDB(index, _username, _password, _location, _airline_tags, _city_tags, _messagebank_ann_id, _broadcastzones)
if (!user.isEqual(editeduser)){
if (db.userDB.UpdateByIndex(index.toInt(), editeduser)){
db.userDB.Resort()
ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated User : $editeduser")
} else ctx.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to update user with index $index")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Nothing has changed for user with index $index")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some broadcast zone is not found in Broadcast Zone list")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some ANN_ID not found in Messagebank")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Some city tags not found in soundbank")))
} else ctx.status(400).result(objectmapper.writeValueAsString("Some airline tags not found in soundbank"))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Username already exists for another user")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Not all fiels have value")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("UpdateByIndex with index=$index has empty body")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("User with index $index not found")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
}
get("ExportXLSX") {
val xlsxdata = db.userDB.Export_XLSX()
if (xlsxdata != null) {
it.header(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
it.header("Content-Disposition", "attachment; filename=\"userdb.xlsx\"")
it.outputStream().use { out ->
xlsxdata.write(out)
}
} else {
it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to export user table to XLSX")))
}
}
post("ImportXLSX") {
val uploaded = it.uploadedFile("file")
if (uploaded == null) {
it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("No file uploaded")))
return@post
}
try {
val xlsx = XSSFWorkbook(uploaded.content())
if (db.userDB.Import_XLSX(xlsx)) {
db.userDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
} else {
it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to import user table from XLSX")))
} }
} catch (e: Exception) { } catch (e: Exception) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid XLSX file"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid XLSX file")))
} }
} }
//TODO kirim list message dan broadcast zones untuk ADD/Edit schedule
get("GetMessageAndBroadcastZones") {
val result = object {
val messages = db.messageDB.List
.filter { mb -> !mb.Message_Detail.contains("[") && !mb.Message_Detail.contains("]")}
.map { mb -> "${mb.Description} [${mb.ANN_ID}]" }
val broadcastzones = db.broadcastDB.List.map { it.description }
}
it.result(objectmapper.writeValueAsString(result))
}
// Kirim list language dari Messagebank berdasarkan ANN_ID
get("GetLanguageList/{ANN_ID}") { get1 ->
//kirim list language dari Messagebank
val langlist = db.messageDB.List
.filter { it.ANN_ID == get1.pathParam("ANN_ID").toInt().toUInt() }
.map { it.Language }.distinct()
get1.result(objectmapper.writeValueAsString(langlist))
}
} }
path("Log") { path("Log") {
get("List") { get1 -> get("List") { get1 ->
@@ -832,24 +1166,20 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
path("BroadcastZones"){ path("BroadcastZones"){
get("List") { get("List") {
// TODO : temporary, convert BroadcastZones to BroadcastZonesHtml, karena harus revisi javascript di Bootstrap Studio
// val newlist: ArrayList<BroadcastZonesHtml> = db.broadcastDB.List.map { xx ->
// BroadcastZonesHtml(
// xx.index,
// xx.description,
// xx.SoundChannel,
// xx.id,
// xx.bp
// )
// } as ArrayList<BroadcastZonesHtml>
it.result(MariaDB.ArrayListtoString(db.broadcastDB.List)) it.result(MariaDB.ArrayListtoString(db.broadcastDB.List))
} }
delete("List"){ get("BroadcastZoneDescriptions") { ctx ->
val value = db.broadcastDB.List
.distinctBy { it.description }
.map { it.description }
ctx.result(objectmapper.writeValueAsString(value))
}
delete("List") {
// truncate broadcast zones table // truncate broadcast zones table
if (db.broadcastDB.Clear()) { if (db.broadcastDB.Clear()) {
db.broadcastDB.Get() db.broadcastDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Broadcast Zones table")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate broadcast zones table"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate broadcast zones table")))
} }
@@ -868,6 +1198,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.broadcastDB.Add(newbp)){ if (db.broadcastDB.Add(newbp)){
db.broadcastDB.Resort() db.broadcastDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS","Added Broadcast Zone: $newbp")
} else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add broadcast zone to database"))) } else it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to add broadcast zone to database")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Relay"))) } else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Relay")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Box"))) } else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Box")))
@@ -883,6 +1214,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.broadcastDB.DeleteByIndex(index.toInt())) { if (db.broadcastDB.DeleteByIndex(index.toInt())) {
db.broadcastDB.Resort() db.broadcastDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Broadcast Zone with index $index")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete broadcast zone with index $index"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete broadcast zone with index $index")))
} }
@@ -927,6 +1259,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.broadcastDB.UpdateByIndex(index.toInt(), bz)) { if (db.broadcastDB.UpdateByIndex(index.toInt(), bz)) {
db.broadcastDB.Resort() db.broadcastDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Broadcast Zone : $bz")
} else it.status(500) } else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update broadcast zone with index $index"))) .result(objectmapper.writeValueAsString(resultMessage("Failed to update broadcast zone with index $index")))
} else it.status(400) } else it.status(400)
@@ -975,11 +1308,15 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
get("List"){ get("List"){
it.result(MariaDB.ArrayListtoString(db.soundchannelDB.List)) it.result(MariaDB.ArrayListtoString(db.soundchannelDB.List))
} }
delete("List"){ get("SoundChannelDescriptions") {
it.result(objectmapper.writeValueAsString(db.Get_SoundChannel_List()))
}
delete("List") {
// truncate sound channel table // truncate sound channel table
if (db.soundchannelDB.Clear()) { if (db.soundchannelDB.Clear()) {
db.soundchannelDB.Get() db.soundchannelDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK"))) it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Sound Channel table")
} else { } else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate sound channel table"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate sound channel table")))
} }
@@ -1013,23 +1350,26 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} else { } else {
// cek apakah ada soundchannel lain yang pakai ip dan channel yang sama // cek apakah ada soundchannel lain yang pakai ip dan channel yang sama
if (db.soundchannelDB.List.any { xx -> xx.index != index && _ip.equals(xx.ip) && _channel.equals(xx.channel, true) }) { val othersc = db.soundchannelDB.List.filter { sc -> sc.ip == _ip }.filter { sc -> sc.index != index }
println("Another sound channel already uses IP $_ip and channel $_channel") if (othersc.isNotEmpty()){
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Another sound channel already uses IP $_ip and channel $_channel"))) it.status(400).result(objectmapper.writeValueAsString(resultMessage("This IP address is already used by another sound channel")))
return@patch } else {
// ada sesuatu yang ganti
val newsc = SoundChannel(0u, _channel, _ip)
if (db.soundchannelDB.UpdateByIndex(index.toInt(), newsc)) {
println("Updated sound channel with index $index")
db.soundchannelDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Sound Channel : $newsc")
} else {
println("Failed to update sound channel with index $index")
it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update sound channel with index $index")))
}
} }
// ada sesuatu yang ganti
val newsc = SoundChannel(0u, _channel, _ip)
if (db.soundchannelDB.UpdateByIndex(index.toInt(),newsc)){
println("Updated sound channel with index $index")
db.soundchannelDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
} else {
println("Failed to update sound channel with index $index")
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to update sound channel with index $index")))
return@patch
}
} }
} else { } else {
@@ -1046,7 +1386,6 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} }
} }
get("ExportXLSX"){ get("ExportXLSX"){
val xlsxdata = db.soundchannelDB.Export_XLSX() val xlsxdata = db.soundchannelDB.Export_XLSX()
@@ -1078,10 +1417,79 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to import sound channel from XLSX"))) it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to import sound channel from XLSX")))
} }
} catch (e: Exception) { } catch (e: Exception) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid XLSX file"))) it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid XLSX file")))
} }
} }
} }
// Steph : coba tambah untuk QueuePaging Table. Belum ada di JS file(?)
path("QueuePaging"){
get("List"){
it.result(MariaDB.ArrayListtoString(db.queuepagingDB.List))
}
delete("List"){
// truncate queue paging table
if (db.queuepagingDB.Clear()) {
db.queuepagingDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear queue paging table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate queue paging table")))
}
}
delete("DeleteByIndex/{index}") {
// delete by index
val index = it.pathParam("index").toUIntOrNull()
if (index == null) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
} else {
if (db.queuepagingDB.DeleteByIndex(index.toInt())) {
db.queuepagingDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted queue paging with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete queue paging with index $index")))
}
}
}
}
// Steph : coba tambah untuk QueueTable Table. Belum ada di JS file(?)
path("QueueTable"){
get("List"){
it.result(MariaDB.ArrayListtoString(db.queuetableDB.List))
}
delete("List"){
// truncate queue table
if (db.queuetableDB.Clear()) {
db.queuetableDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Automatic Queue Table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate queue sound table")))
}
}
delete("DeleteByIndex/{index}") {
// delete by index
val index = it.pathParam("index").toUIntOrNull()
if (index == null) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
} else {
if (db.queuetableDB.DeleteByIndex(index.toInt())) {
db.queuetableDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Automatic Queue Table with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete queue sound with index $index")))
}
}
}
}
} }
} }
}.start(listenPort) }.start(listenPort)