Compare commits

...

54 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
cf69c72f3c commit 06/10/2025 2025-10-06 08:30:09 +07:00
20dbc12b02 commit 02/10/2025 2025-10-02 16:03:28 +07:00
3768f4263b commit 02/10/2025 2025-10-02 13:41:26 +07:00
1e7adeba25 commit 02/10/2025 2025-10-02 13:17:52 +07:00
rdkartono
83a6ee9fd0 Merge branch 'master' of https://gitea.rdkartono.my.id/rdkartono/AAS_NewGeneration 2025-10-01 15:52:31 +07:00
rdkartono
e0f3ac2094 Commit 30/09/2025 2025-10-01 15:50:33 +07:00
c55db5e4f7 commit 01/10/2025 2025-10-01 13:57:20 +07:00
81 changed files with 6636 additions and 1740 deletions

6
.gitignore vendored
View File

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

View File

@@ -1,11 +1,11 @@
<component name="libraryTable">
<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>
<root url="jar://$MAVEN_REPOSITORY$/com/github/oshi/oshi-core/6.6.2/oshi-core-6.6.2.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-platform/5.14.0/jna-platform-5.14.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$/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.17.0/jna-5.17.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.17/slf4j-api-2.0.17.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />

View File

@@ -1,8 +1,8 @@
<component name="libraryTable">
<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>
<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>
<JAVADOC />
<SOURCES />

View File

@@ -1,9 +1,9 @@
<component name="libraryTable">
<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>
<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-api/2.6.2/tinylog-api-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.7.0/tinylog-api-2.7.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />

View File

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

@@ -60,3 +60,20 @@
align-items: center;
}
.pad-relay {
padding-left: 1.5rem;
padding-top: 0.5rem;
}
.pad-time {
margin: 0.7rem auto;
}
.pad-day {
margin-top: 0.5rem;
}
.class100 {
width: 100% !important;
}

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

@@ -1,5 +1,6 @@
body {
background-color: #f8f9fd;
/*background-color: #edf1fb;*/
overflow-x: hidden;
width: 100%;
}
@@ -13,8 +14,9 @@ body {
}
.search {
display: flex;
align-items: center;
/*display: flex;*/
/*align-items: center;*/
margin-top: -0.2rem;
}
.text-header {
@@ -60,9 +62,9 @@ body {
.btn-round-basic:focus {
background-color: #f5f5f5;
border-radius: 20px;
border-radius: 8px;
box-shadow: inset 4px 4px 10px #88a5bf7b, inset -4px -4px 10px #ffffff;
color: #4d4d4d;
/*color: #4d4d4d;*/
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease-in-out;
@@ -70,7 +72,7 @@ body {
}
.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;
--bs-btn-hover-bg: #ffffff;
}
@@ -80,29 +82,29 @@ body {
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-remove {
.color-remove, .color-remove:hover, .color-remove:focus {
color: var(--bs-danger);
}
.color-edit {
.color-edit, .color-edit:hover, .color-edit:focus {
color: var(--bs-primary-text-emphasis);
}
.color-add {
.color-add, .color-add:hover, .color-add:focus {
color: var(--bs-primary);
}
.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;
}
.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;
--bs-btn-hover-bg: #5780f2;
background-color: #5278e1;
@@ -188,3 +190,131 @@ nav-item:focus {
border: none;
}
.pad-card {
padding-top: 1rem;
}
.pad-search {
padding-top: 0.5rem;
}
.pad-row-search {
margin-bottom: 0.7rem;
}
.bg-heading1 {
background-color: #c6d8ee;
color: #2d3578;
}
.bg-heading2 {
background-color: #dce5f4;
color: #2d3578;
}
.bg-heading3 {
background-color: #e9ecf8;
color: #2d3578;
}
.accordion-item {
/*background: rgba(255, 255, 255, 0.55);*/
/*backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);*/
/*border-radius: 16px;*/
/*box-shadow: 8px 8px 16px rgba(0, 0, 0, 0.12), -8px -8px 16px rgba(255, 255, 255, 0.6);*/
/*transition: all 0.3s ease;*/
}
.accordion-item.active {
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);
}
.bg-accordion {
border: white 2px;
border-radius: 8px;
background: #f8f9fd;
}
.card-channel {
border-radius: 8px;
background: #ffffff;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: #ffffff solid 2px !important;
padding-top: 12px;
padding-bottom: 12px;
}
.pad-accordion {
border-radius: 40px;
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

@@ -11,13 +11,19 @@
* List of broadcast zones available
* @type {BroadcastZone[]}
*/
let BroadcastZoneList = [];
window.BroadcastZoneList ??= [];
/**
* Currently selected broadcast zone row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedBroadcastZoneRow = null;
window.selectedBroadcastZoneRow = null;
/**
* List of sound channels available
* @type {String[]}
*/
window.SoundChannelList = []
/**
* Fill broadcast zone table body with values
@@ -31,23 +37,23 @@ function fill_broadcastzonetablebody(vv) {
<td>${item.index}</td>
<td>${item.description}</td>
<td>${item.soundChannel}</td>
<td>${item.box}</td>
<td>${item.relay}</td>
<td>${item.id}</td>
<td>${item.bp}</td>
</tr>`;
$('#broadcastzonetablebody').append(row);
let $addedrow = $('#broadcastzonetablebody tr:last');
$addedrow.click(function () {
if (selectedBroadcastZoneRow) {
selectedBroadcastZoneRow.find('td').css('background-color', '');
if (selectedBroadcastZoneRow.is($(this))) {
selectedBroadcastZoneRow = null;
$addedrow.off('click').on('click', function () {
if (window.selectedBroadcastZoneRow) {
window.selectedBroadcastZoneRow.find('td').css('background-color', '');
if (window.selectedBroadcastZoneRow.is($(this))) {
window.selectedBroadcastZoneRow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
selectedBroadcastZoneRow = $(this);
window.selectedBroadcastZoneRow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -60,21 +66,33 @@ function fill_broadcastzonetablebody(vv) {
* @param {String} APIURL API URL endpoint (default "BroadcastZones/")
*/
function reloadBroadcastZones(APIURL = "BroadcastZones/") {
BroadcastZoneList = [];
window.BroadcastZoneList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadBroadcastZones : ", okdata)
BroadcastZoneList = okdata;
fill_broadcastzonetablebody(BroadcastZoneList);
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 () {
console.log("broadcastzones.js loaded successfully");
selectedBroadcastZoneRow = null;
window.selectedBroadcastZoneRow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnEdit = $('#btnEdit');
@@ -95,15 +113,15 @@ $(document).ready(function () {
let $findzone = $('#findzone');
$findzone.on('input', function () {
$findzone.off('input').on('input', function () {
let searchTerm = $findzone.val().trim().toLowerCase();
if (searchTerm.length > 0) {
selectedBroadcastZoneRow = null;
let filtered = broadcastzonedata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.box.toLowerCase().includes(searchTerm) || item.soundChannel.toLowerCase().includes(searchTerm) || item.relay.toLowerCase().includes(searchTerm));
window.selectedBroadcastZoneRow = null;
let filtered = window.BroadcastZoneList.filter(item => item.description.toLowerCase().includes(searchTerm) || item.id.toLowerCase().includes(searchTerm) || item.soundChannel.toLowerCase().includes(searchTerm) || item.bp.toLowerCase().includes(searchTerm));
fill_broadcastzonetablebody(filtered);
} else {
selectedBroadcastZoneRow = null;
fill_broadcastzonetablebody(broadcastzonedata);
window.selectedBroadcastZoneRow = null;
fill_broadcastzonetablebody(window.BroadcastZoneList);
}
});
@@ -125,24 +143,26 @@ $(document).ready(function () {
$broadcastzonedescription.val('');
// fill broadcastzonesoundchannel from SoundChannelList
$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.forEach(ch => {
if (ch.channel && ch.channel.length > 0){
// hanya yang punya channel saja
$broadcastzonesoundchannel.append($('<option>').val(ch.channel).text(ch.channel));
window.SoundChannelList.forEach(ch => {
if (ch && ch.length>0){
$broadcastzonesoundchannel.append($('<option>').val(ch).text(ch));
}
});
}
$broadcastzonebox.val('');
$broadcastzonebox.val('1').prop('disabled', true);
for (let i = 1; i <= 32; i++) {
cbRelay(i).prop('checked', false);
}
}
fetchSoundChannels();
reloadBroadcastZones(APIURL_BroadcastZone);
$btnClear.click(() => {
$btnClear.off('click').on('click', () => {
DoClear(APIURL_BroadcastZone, "BroadcastZones", (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success clear broadcast zones: " + okdata.message);
@@ -151,7 +171,7 @@ $(document).ready(function () {
});
});
$btnAdd.click(() => {
$btnAdd.off('click').on('click', () => {
$broadcastzonemodal.modal('show');
clearBroadcastZoneModal();
@@ -203,12 +223,12 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedBroadcastZoneRow) {
let cells = selectedBroadcastZoneRow.find('td');
$btnRemove.off('click').on('click', () => {
if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */
let bz = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
SoundChannel: cells.eq(2).text(),
Box: cells.eq(3).text(),
@@ -225,29 +245,29 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedBroadcastZoneRow) {
let cells = selectedBroadcastZoneRow.find('td');
$btnEdit.off('click').on('click', () => {
if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */
let bz = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
SoundChannel: cells.eq(2).text(),
Box: cells.eq(3).text(),
Relay: cells.eq(4).text()
};
if (confirm(`Are you sure to edit broadcast zone [${bz.index}] Description=${bz.description} SoundChannel=${bz.SoundChannel} Box=${bz.Box} Relay=${bz.Relay}?`)) {
if (confirm(`Are you sure to edit broadcast zone [${bz.index}] Description=${bz.description} SoundChannel=${bz.SoundChannel} Box=${bz.id} Relay=${bz.bp}?`)) {
$broadcastzonemodal.modal('show');
clearBroadcastZoneModal();
$broadcastzoneindex.val(bz.index);
$broadcastzonedescription.val(bz.description);
$broadcastzonesoundchannel.val(bz.SoundChannel);
$broadcastzonebox.val(bz.Box);
if (bz.Relay) {
bz.Relay.split(';').forEach(relayId => {
let id = parseInt(relayId, 10);
cbRelay(id).prop('checked', true);
if (bz.Relay && bz.Relay.length > 0) {
bz.Relay.split(';').map(Number).filter(n => !isNaN(n) && n>=1 && n<=8).forEach(relayId => {
cbRelay(relayId).prop('checked', true);
});
}
$broadcastzonemodal.off('click.broadcastzonesave').on('click.broadcastzonesave', '#broadcastzonesave', function () {
let description = $broadcastzonedescription.val().trim();
@@ -297,11 +317,11 @@ $(document).ready(function () {
}
});
$btnExport.click(() => {
$btnExport.off('click').on('click', () => {
DoExport(APIURL_BroadcastZone, "broadcastzones.xlsx", {});
});
$btnImport.click(() => {
$btnImport.off('click').on('click', () => {
DoImport(APIURL_BroadcastZone, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success import broadcast zones: " + okdata.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

@@ -9,12 +9,12 @@
/** List of Languagebank data loaded from server
* @type {LanguageBank[]}
*/
let languagebankdata = [];
window.languagebankdata = [];
/**
* Currently selected languagebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedlanguagerow = null;
window.selectedlanguagerow = null;
/**
* Fill languagebank table body with values
@@ -32,17 +32,17 @@ function fill_languagebanktablebody(vv) {
$('#languagebanktablebody').append(row);
let $addedrow = $('#languagebanktablebody tr:last');
$addedrow.click(function () {
if (selectedlanguagerow) {
selectedlanguagerow.find('td').css('background-color', '');
if (selectedlanguagerow.is($(this))) {
selectedlanguagerow = null;
if (window.selectedlanguagerow) {
window.selectedlanguagerow.find('td').css('background-color', '');
if (window.selectedlanguagerow.is($(this))) {
window.selectedlanguagerow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$addedrow.find('td').css('background-color', '#ffeeba');
selectedlanguagerow = $addedrow;
window.selectedlanguagerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -55,12 +55,12 @@ function fill_languagebanktablebody(vv) {
* @param {string} APIURL API URL endpoint, default "LanguageLink/"
*/
function reloadLanguageBank(APIURL = "LanguageLink/") {
languagebankdata = [];
window.languagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
languagebankdata = okdata;
selectedlanguagerow = null;
fill_languagebanktablebody(languagebankdata);
window.languagebankdata.push(...okdata);
window.selectedlanguagerow = null;
fill_languagebanktablebody(window.languagebankdata);
}
}, (errdata) => {
alert("Error loading languagebank : " + errdata.message);
@@ -71,7 +71,7 @@ $(document).ready(function () {
console.log('languagebank.js loaded');
$('#languagebanktablebody').empty();
selectedlanguagerow = null;
window.selectedlanguagerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
@@ -106,12 +106,12 @@ $(document).ready(function () {
$findlanguage.on('input', function () {
let searchTerm = $findlanguage.val().toLowerCase();
if (searchTerm.length > 0) {
selectedlanguagerow = null;
let filtered = languagebankdata.filter(item => item.tag.toLowerCase().includes(searchTerm) || item.language.toLowerCase().includes(searchTerm));
window.selectedlanguagerow = null;
let filtered = window.languagebankdata.filter(item => item.tag.toLowerCase().includes(searchTerm) || item.language.toLowerCase().includes(searchTerm));
fill_languagebanktablebody(filtered);
} else {
selectedlanguagerow = null;
fill_languagebanktablebody(languagebankdata);
window.selectedlanguagerow = null;
fill_languagebanktablebody(window.languagebankdata);
}
});
@@ -171,11 +171,11 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedlanguagerow) {
let cells = selectedlanguagerow.find('td');
if (window.selectedlanguagerow) {
let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */
let ll = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
tag: cells.eq(1).text(),
language: cells.eq(2).text()
}
@@ -190,11 +190,11 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedlanguagerow) {
let cells = selectedlanguagerow.find('td');
if (window.selectedlanguagerow) {
let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */
let ll = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
tag: cells.eq(1).text(),
language: cells.eq(2).text()
}

View File

@@ -10,7 +10,7 @@
/** List of Log data loaded from server
* @type {Log[]}
*/
let logdata = [];
window.logdata = [];
/**
* Fill log table body with values
@@ -18,9 +18,11 @@ let logdata = [];
*/
function fill_logtablebody(vv) {
$('#logtablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) {
$('#btnExport').prop('disabled', true);
$('#searchfilter').prop('disabled', true);
if (!Array.isArray(vv) || vv.length === 0) return;
return;
}
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
@@ -33,7 +35,6 @@ function fill_logtablebody(vv) {
});
$('#tablesize').text("Table Size: " + vv.length);
$('#btnExport').prop('disabled', false);
$('#searchfilter').prop('disabled', false);
}
/**
@@ -47,10 +48,11 @@ function reloadLogs(APIURL = "Log/", date, filter) {
date: date,
filter: filter
})
window.logdata = [];
fetchAPI(APIURL + "List?" + params.toString(), "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
logdata = okdata;
fill_logtablebody(okdata);
window.logdata.push(...okdata);
fill_logtablebody(window.logdata);
}
}, (errdata) => {
alert("Error loading logs : " + errdata.message);
@@ -59,26 +61,22 @@ function reloadLogs(APIURL = "Log/", date, filter) {
$(document).ready(function () {
console.log("log.js ready");
const $logdate = $('#logdate');
const $searchfilter = $('#searchfilter');
const $logtable = $('#logtablebody')
const $btnExport = $('#btnExport');
let selectedlogdate = "";
let logfilter = "";
let APIURL = "Log/";
$logtable.empty();
$('#logtablebody').empty();
if (!$logdate.val()) {
if (!$('#logdate').val()) {
const today = new Date();
const dd = String(today.getDate()).padStart(2, '0');
const mm = String(today.getMonth() + 1).padStart(2, '0');
const yyyy = today.getFullYear();
$logdate.val(`${yyyy}-${mm}-${dd}`);
$('#logdate').val(`${yyyy}-${mm}-${dd}`);
selectedlogdate = `${dd}-${mm}-${yyyy}`;
reloadLogs(APIURL, selectedlogdate, logfilter);
}
$logdate.off('change').on('change', function () {
$('#logdate').off('change').on('change', function () {
const selected = $(this).val();
if (selected) {
const [year, month, day] = selected.split('-');
@@ -87,11 +85,11 @@ $(document).ready(function () {
}
});
$searchfilter.off('input').on('input', function () {
$('#searchfilter').off('input').on('input', function () {
logfilter = $(this).val();
reloadLogs(APIURL, selectedlogdate, logfilter);
});
$btnExport.off('click').on('click', function () {
$('#btnExport').off('click').on('click', function () {
DoExport(APIURL, "log.xlsx", { date: selectedlogdate, filter: logfilter });
});
});

View File

@@ -13,12 +13,12 @@
* List of Messagebank data loaded from server
* @type {MessageBank[]}
*/
let messagebankdata = [];
window.messagebankdata ??= [];
/**
* Currently selected messagebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedmessagerow = null;
window.selectedmessagerow = null;
/**
* Fill messagebank table body with values
@@ -40,17 +40,17 @@ function fill_messagebanktablebody(vv) {
$('#messagebanktablebody').append(row);
let $addedrow = $('#messagebanktablebody tr:last');
$addedrow.click(function () {
if (selectedmessagerow) {
selectedmessagerow.find('td').css('background-color', '');
if (selectedmessagerow.is($(this))) {
selectedmessagerow = null;
if (window.selectedmessagerow) {
window.selectedmessagerow.find('td').css('background-color', '');
if (window.selectedmessagerow.is($(this))) {
window.selectedmessagerow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$addedrow.find('td').css('background-color', '#ffeeba');
selectedmessagerow = $addedrow;
window.selectedmessagerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -64,12 +64,12 @@ function fill_messagebanktablebody(vv) {
* @param {string} APIURL API URL endpoint, default "MessageBank/"
*/
function reloadMessageBank(APIURL = "MessageBank/") {
messagebankdata = [];
window.messagebankdata ??= [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
messagebankdata = okdata;
selectedmessagerow = null;
fill_messagebanktablebody(messagebankdata);
window.messagebankdata.push(...okdata);
window.selectedmessagerow = null;
fill_messagebanktablebody(window.messagebankdata);
}
}, (errdata) => {
alert("Error loading messagebank : " + errdata.message);
@@ -79,7 +79,7 @@ function reloadMessageBank(APIURL = "MessageBank/") {
$(document).ready(function () {
console.log("messagebank.js loaded");
$('#messagebanktablebody').empty();
selectedmessagerow = null;
window.selectedmessagerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
@@ -121,17 +121,32 @@ $(document).ready(function () {
function refill_messageavailablevariables() {
$messageavailablevariables.empty();
categories.forEach(cat => {
$messageavailablevariables.append(ListItem(`{${cat}}`));
$messageavailablevariables.append(ListItem(`[${cat}]`));
});
if ($messagelanguage.val() && $messagevoicetype.val()) {
soundbankdata
.filter(sb => sb.language.toLowerCase() === $messagelanguage.val().toLowerCase())
.filter(sb => sb.voiceType.toLowerCase() === $messagevoicetype.val().toLowerCase())
.filter(sb => sb.category.toLowerCase() === "phrase")
.forEach(sb => {
$messageavailablevariables.append(ListItem(`[${sb.Description}]`));
let lang = $messagelanguage.val();
let vt = $messagevoicetype.val();
if (lang && lang.length > 0){
console.log("Selected Language:", lang);
if (vt && vt.length > 0){
console.log("Selected Voice Type:", vt);
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);
});
}
}
}
@@ -143,7 +158,7 @@ $(document).ready(function () {
$messagedescription.val('');
// fill messagelanguage options from languages[]
$messagelanguage.empty();
languages.forEach(lang => {
window.languages.forEach(lang => {
$messagelanguage.append(new Option(lang, lang));
});
$messagelanguage.val(null);
@@ -154,7 +169,7 @@ $(document).ready(function () {
$messageannid.val(1);
// fill messagevoicetype options from voiceTypes[]
$messagevoicetype.empty();
voiceTypes.forEach(vt => {
window.voiceTypes.forEach(vt => {
$messagevoicetype.append(new Option(vt, vt));
});
$messagevoicetype.val(null);
@@ -198,12 +213,12 @@ $(document).ready(function () {
$findmessage.on('input', function () {
let searchTerm = $findmessage.val().toLowerCase();
if (searchTerm.length > 0) {
selectedmessagerow = null;
let filtered = messagebankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.message_Detail.toLowerCase().includes(searchTerm) || item.message_TAGS.toLowerCase().includes(searchTerm));
window.selectedmessagerow = null;
let filtered = window.messagebankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.message_Detail.toLowerCase().includes(searchTerm) || item.message_TAGS.toLowerCase().includes(searchTerm));
fill_messagebanktablebody(filtered);
} else {
selectedmessagerow = null;
fill_messagebanktablebody(messagebankdata);
window.selectedmessagerow = null;
fill_messagebanktablebody(window.messagebankdata);
}
});
@@ -301,11 +316,11 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedmessagerow) {
let cells = selectedmessagerow.find('td');
if (window.selectedmessagerow) {
let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */
let mb = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
language: cells.eq(2).text(),
aNN_ID: parseInt(cells.eq(3).text()),
@@ -325,11 +340,11 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedmessagerow) {
let cells = selectedmessagerow.find('td');
if (window.selectedmessagerow) {
let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */
let mb = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
language: cells.eq(2).text(),
aNN_ID: parseInt(cells.eq(3).text()),
@@ -344,19 +359,8 @@ $(document).ready(function () {
// Fill modal fields with selected messagebank data
$messageindex.val(mb.index).prop('disabled', true);
$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);
// Fill messagevoicetype options and select current
$messagevoicetype.empty();
voiceTypes.forEach(vt => {
$messagevoicetype.append(new Option(vt, vt));
});
$messagevoicetype.val(mb.voice_Type);
// Set annid
$messageannid.val(mb.aNN_ID);
// Refill message available variables
refill_messageavailablevariables();

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

@@ -14,12 +14,12 @@
/** List of Schedulebank data loaded from server
* @type {ScheduleBank[]}
*/
let schedulebankdata = [];
window.schedulebankdata = [];
/**
* Currently selected schedulebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedschedulerow = null;
window.selectedschedulerow = null;
/**
* Fill schedulebank table body with values
@@ -66,12 +66,12 @@ function fill_schedulebanktablebody(vv) {
* @param {string} APIURL API URL endpoint, default "ScheduleBank/"
*/
function reloadTimerBank(APIURL = "ScheduleBank/") {
schedulebankdata = [];
window.schedulebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
schedulebankdata = okdata;
window.schedulebankdata.push(...okdata);
selectedschedulerow = null;
fill_schedulebanktablebody(schedulebankdata);
fill_schedulebanktablebody(window.schedulebankdata);
}
}, (errdata) => {
alert("Error loading schedulebank : " + errdata.message);
@@ -101,28 +101,30 @@ $(document).ready(function () {
let $schedulehour = $schedulemodal.find('#schedulehour');
// number input 0-59
let $scheduleminute = $schedulemodal.find('#scheduleminute');
// text input
let $schedulesoundpath = $schedulemodal.find('#schedulesoundpath');
// select2 for message
let $schedulemessage = $schedulemodal.find('#schedulemessage');
$schedulemessage.select2({});
// number input 0-5
let $schedulerepeat = $schedulemodal.find('#schedulerepeat');
// checkbox
let $scheduleenable = $schedulemodal.find('#scheduleenable');
// div for list of checkboxes
// select2 for broadcastzones
let $schedulezones = $schedulemodal.find('#schedulezones');
$schedulezones.select2({});
// radio button for everyday
let $scheduleeveryday = $schedulemodal.find('#scheduleeveryday');
// radio button for weekdays
let $schedulesunday = $schedulemodal.find('#schedulesunday');
let $schedulemonday = $schedulemodal.find('#schedulemonday');
let $scheduletuesday = $schedulemodal.find('#scheduletuesday');
let $schedulewednesday = $schedulemodal.find('#schedulewednesday');
let $schedulethursday = $schedulemodal.find('#schedulethursday');
let $schedulefriday = $schedulemodal.find('#schedulefriday');
let $schedulesaturday = $schedulemodal.find('#schedulesaturday');
// radio button for weekly
let $scheduleweekly = $schedulemodal.find('#scheduleweekly');
// select2 for weekly selection
let $weeklyselect = $schedulemodal.find('#weeklyselect');
$weeklyselect.select2({});
// radio button for specific date
let $schedulespecialdate = $schedulemodal.find('#schedulespecialdate');
// date input
let $scheduledate = $schedulemodal.find('#scheduledate');
// select2 for language
let $languageselect = $schedulemodal.find('#languageselect');
$languageselect.select2({});
$schedulespecialdate.on('change', function () {
if ($(this).is(':checked')) {
@@ -137,17 +139,10 @@ $(document).ready(function () {
$scheduledescription.val('');
$schedulehour.val('0');
$scheduleminute.val('0');
$schedulesoundpath.val('');
$schedulerepeat.val('0');
$scheduleenable.prop('checked', true);
$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);
$scheduledate.prop('disabled', true).val('');
@@ -158,15 +153,15 @@ $(document).ready(function () {
$findschedule.on('input', function () {
let searchTerm = $findschedule.val().toLowerCase();
if (searchTerm.length > 0) {
selectedtimerow = null;
let filtered = schedulebankdata.filter(item =>
window.selectedschedulerow = null;
let filtered = window.schedulebankdata.filter(item =>
item.description.toLowerCase().includes(searchTerm)
|| item.soundpath.toLowerCase().includes(searchTerm)
|| item.broadcastZones.toLowerCase().includes(searchTerm));
fill_schedulebanktablebody(filtered);
} else {
selectedtimerow = null;
fill_schedulebanktablebody(schedulebankdata);
window.selectedschedulerow = null;
fill_schedulebanktablebody(window.schedulebankdata);
}
});
@@ -246,11 +241,11 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedtimerow) {
let cells = selectedtimerow.find('td');
if (window.selectedschedulerow) {
let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */
let sr = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
day: cells.eq(2).text(),
time: cells.eq(3).text(),
@@ -271,11 +266,11 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedtimerow) {
let cells = selectedtimerow.find('td');
if (window.selectedschedulerow) {
let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */
let sr = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
day: cells.eq(2).text(),
time: cells.eq(3).text(),

View File

@@ -2,23 +2,23 @@
* List of voice types available
* @type {string[]}
*/
let voiceTypes = [];
window.voiceTypes = [];
/**
* List of categories available
* @type {string[]}
*/
let categories = [];
window.categories = [];
/**
* List of languages available
* @type {string[]}
*/
let languages = [];
window.languages = [];
/**
* List of scheduled days available
* @type {string[]}
*/
let scheduledays = []
window.scheduledays = []
/**
* Create a list item element
@@ -34,7 +34,7 @@ function ListItem(text, className = "") {
* WebSocket connection
* @type {WebSocket}
*/
let ws = null;
window.ws = null;
/**
* Send a command to the WebSocket server.
@@ -42,8 +42,8 @@ let ws = null;
* @param {String} data data to send
*/
function sendCommand(command, data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command, data }));
if (window.ws.readyState === WebSocket.OPEN) {
window.ws.send(JSON.stringify({ command, data }));
}
}
@@ -118,11 +118,11 @@ function fetchImg(url, cbOK, cbError) {
* Reload voice types from server
*/
function getVoiceTypes() {
voiceTypes = [];
window.voiceTypes = [];
fetchAPI("VoiceType", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
voiceTypes = okdata.filter(item => item.trim().length > 0);
window.voiceTypes = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + voiceTypes.length + " voice types : " + voiceTypes.join(", "));
} else console.log("getVoiceTypes: okdata is not array");
}, (errdata) => {
@@ -134,11 +134,11 @@ function getVoiceTypes() {
* Reload categories from server
*/
function getCategories() {
categories = [];
window.categories = [];
fetchAPI("Category", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
categories = okdata.filter(item => item.trim().length > 0);
window.categories = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + categories.length + " categories : " + categories.join(", "));
} else console.log("getCategories: okdata is not array");
}, (errdata) => {
@@ -150,11 +150,11 @@ function getCategories() {
* Reload languages from server
*/
function getLanguages() {
languages = [];
window.languages = [];
fetchAPI("Language", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
languages = okdata.filter(item => item.trim().length > 0);
window.languages = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + languages.length + " languages : " + languages.join(", ") );
} else console.log("getLanguages: okdata is not array");
}, (errdata) => {
@@ -166,11 +166,11 @@ function getLanguages() {
* Reload scheduled days from server
*/
function getScheduledDays() {
scheduledays = [];
window.scheduledays = [];
fetchAPI("ScheduleDay", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
scheduledays = okdata.filter(item => item.trim().length > 0);
window.scheduledays = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + scheduledays.length + " scheduled days : " + scheduledays.join(", ") );
} else console.log("getScheduledDays: okdata is not array");
}, (errdata) => {
@@ -273,43 +273,36 @@ function DoImport(APIURL, cbOK, cbError) {
fileInput.remove();
}
let $onlineindicator = null;
let $cpustatus = null;
let $ramstatus = null;
let $diskstatus = null;
let $networkstatus = null;
let $datetimetext = null;
let greencircle = null;
let redcircle = null;
window.greencircle = null;
window.redcircle = null;
/**
* App entry point
*/
$(document).ready(function () {
document.title = "Automatic Announcement System"
fetchImg('green_circle.png', (url) => { greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); });
fetchImg('red_circle.png', (url) => { redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); });
if (window.greencircle === null) {
fetchImg('green_circle.png', (url) => { window.greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); });
}
if (window.redcircle === null) {
fetchImg('red_circle.png', (url) => { window.redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); });
}
const wsURL = window.location.pathname + '/ws'
if (chrome && chrome.runtime && chrome.runtime.lastError) {
alert("Runtime error: " + chrome.runtime.lastError.message);
return;
}
$onlineindicator = $('#onlineindicator');
$cpustatus = $('#cpustatus');
$ramstatus = $('#ramstatus');
$diskstatus = $('#diskstatus');
$networkstatus = $('#networkstatus');
$datetimetext = $('#datetimetext');
// reset status indicators
function resetStatusIndicators() {
$onlineindicator.attr('src', redcircle);
$cpustatus.text("CPU : N/A");
$ramstatus.text("RAM : N/A");
$diskstatus.text("Disk : N/A");
$networkstatus.text("Network : N/A");
$datetimetext.text("Date/Time : N/A");
$('#onlineindicator').attr('src', window.redcircle);
$('#cpustatus').text("CPU : N/A");
$('#ramstatus').text("RAM : N/A");
$('#diskstatus').text("Disk : N/A");
$('#networkstatus').text("Network : N/A");
$('#datetimetext').text("Date/Time : N/A");
}
@@ -320,49 +313,81 @@ $(document).ready(function () {
getScheduledDays();
// Initialize WebSocket connection
ws = new WebSocket(wsURL);
// reconnect handle
let ws_reconnect;
ws.onopen = () => {
function reconnect() {
if (window.ws && window.ws.readyState === WebSocket.OPEN) return;
const s = new WebSocket(wsURL);
s.addEventListener('open', () => {
console.log('WebSocket connection established');
$onlineindicator.attr('src', greencircle);
};
ws.onmessage = (event) => {
$('#onlineindicator').attr('src', window.greencircle);
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)
$('#cpustatus').text("CPU : " + data)
break;
case "getMemoryStatus":
$ramstatus.text("RAM : " + data)
$('#ramstatus').text("RAM : " + data)
break;
case "getDiskStatus":
$diskstatus.text("Disk : " + data)
$('#diskstatus').text("Disk : " + data)
break;
case "getNetworkStatus":
$networkstatus.text("Network : " + data)
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)
$('#datetimetext').text(data)
break;
}
}
};
ws.onclose = () => {
console.log('WebSocket connection closed');
resetStatusIndicators();
};
// ws.onerror = (error) => {
// console.error('WebSocket error:', error);
// };
});
window.ws = s;
}
reconnect();
window.addEventListener('beforeunload', () => {
try{
window.ws?.close(1000, "Client closed connection");
} catch (error) {
console.error("Error closing WebSocket connection:", error);
}
});
setInterval(() => {
sendCommand("getCPUStatus", "")
@@ -457,6 +482,18 @@ $(document).ready(function () {
}
});
})
$('#usermanagement').click(() => {
sidemenu.hide();
$('#content').load('usermanagement.html', function (response, status, xhr) {
if (status === "success") {
console.log("User Management content loaded successfully");
// pindah ke usermanagement.js
} else {
console.error("Error loading user management content:", xhr.status, xhr.statusText);
}
});
});
$('#settinglink').click(() => {
sidemenu.hide();
$('#content').load('setting.html', function (response, status, xhr) {

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,3 +1,11 @@
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* @typedef {Object} SoundBank
* @property {number} index
@@ -9,48 +17,38 @@
* @property {string} path
*/
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* List of Soundbank data loaded from server
* @type {SoundBank[]}
*/
let soundbankdata = [];
window.soundbankdata = [];
/**
* Currently selected soundbank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedsoundrow = null;
window.selectedsoundrow = null;
/**
* List of sound files in the soundbank directory, that ends with .wav or .mp3
* @type {string[]}
*/
let soundbankfiles = [];
/**
* Select2 data source
* See https://select2.org/data-sources/formats
* @type {Select2item[]}
*/
let select2data = [];
window.select2data = [];
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
*/
function reloadSoundBank(APIURL = "SoundBank/") {
soundbankdata = [];
window.soundbankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
soundbankdata = okdata;
selectedsoundrow = null;
fill_soundbanktablebody(soundbankdata);
window.soundbankdata.push(...okdata);
window.selectedsoundrow = null;
fill_soundbanktablebody(window.soundbankdata);
}
}, (errdata) => {
alert("Error loading soundbank : " + errdata.message);
@@ -77,17 +75,17 @@ function fill_soundbanktablebody(vv) {
$('#soundbanktablebody').append(row);
let $addedrow = $('#soundbanktablebody tr:last');
$addedrow.on('click', function () {
if (selectedsoundrow) {
selectedsoundrow.find('td').css('background-color', '');
if (selectedsoundrow.is($(this))) {
selectedsoundrow = null;
if (window.selectedsoundrow) {
window.selectedsoundrow.find('td').css('background-color', '');
if (window.selectedsoundrow.is($(this))) {
window.selectedsoundrow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
selectedsoundrow = $(this);
window.selectedsoundrow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -97,30 +95,62 @@ function fill_soundbanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload soundbank files from server
* @param {String} APIURL API URL endpoint (default "SoundBank/")
* 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(APIURL = "SoundBank/") {
soundbankfiles = [];
fetchAPI(APIURL + "ListFiles", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
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)){
soundbankfiles = okdata.filter(item => item.trim().length > 0);
// refill select2data
select2data = soundbankfiles.map((item, index) => ({ id: index + 1, text: item }));
} else console.log("reloadSoundbankFiles: okdata is not array");
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 () {
console.log("soundbank.js loaded successfully");
reloadSoundbankFiles();
$('#soundbanktablebody').empty();
selectedsoundrow = null;
window.selectedsoundrow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
@@ -137,7 +167,9 @@ $(document).ready(function () {
let $modalcategory = $modal.find('#modalcategory');
let $modallanguage = $modal.find('#modallanguage');
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
@@ -164,24 +196,20 @@ $(document).ready(function () {
$modalvoicetype.append(new Option(vt, vt));
});
$modalvoicetype.val(null);
// fill modalpath options from soundbankfiles[]
// TODO read https://jeesite.com/front/jquery-select2/4.0/index.htm
console.log("select2data has " + select2data.length + " items");
$('#modalpath').select2({
data: select2data
})
$('#modalpath').select2()
}
reloadSoundBank(APIURL);
$('#findsoundbank').on('input', function () {
let searchTerm = $(this).val().trim().toLowerCase();
if (searchTerm.length > 0) {
selectedsoundrow = null;
let filtered = soundbankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.tag.toLowerCase().includes(searchTerm) || item.path.toLowerCase().includes(searchTerm));
window.selectedsoundrow = null;
let filtered = window.soundbankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.tag.toLowerCase().includes(searchTerm) || item.path.toLowerCase().includes(searchTerm));
fill_soundbanktablebody(filtered);
} else {
selectedsoundrow = null;
fill_soundbanktablebody(soundbankdata);
window.selectedsoundrow = null;
fill_soundbanktablebody(window.soundbankdata);
}
});
$btnClear.click(() => {
@@ -193,14 +221,81 @@ $(document).ready(function () {
});
});
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(() => {
$modal.modal('show');
clearSoundbankModal();
// event on selection change of language, category, voiceType
SetupEventForCategoryLanguageVoiceType();
// event on Click save button
$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');
/**
* @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
$modal.off('click.soundbankclose').on('click.soundbankclose', '#soundbankclose', function () {
@@ -208,11 +303,11 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedsoundrow) {
let cells = selectedsoundrow.find('td');
if (window.selectedsoundrow) {
let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */
let sb = {
index: cells.eq(0).text(),
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
tag: cells.eq(2).text(),
category: cells.eq(3).text(),
@@ -231,32 +326,95 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedsoundrow) {
let cells = selectedsoundrow.find('td');
if (window.selectedsoundrow) {
let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */
let sb = {
index: cells.eq(0).text(),
description: cells.eq(1).text(),
tag: cells.eq(2).text(),
category: cells.eq(3).text(),
language: cells.eq(4).text(),
voiceType: cells.eq(5).text(),
path: cells.eq(6).text()
index: Number(cells.eq(0).text()),
Description: cells.eq(1).text(),
TAG: cells.eq(2).text(),
Category: cells.eq(3).text(),
Language: cells.eq(4).text(),
VoiceType: cells.eq(5).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.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
$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');
});
// event on Click close button
$modal.off('click.soundbankclose').on('click.soundbankclose', '#soundbankclose', function () {
$modal.modal('hide');
});
});
}
}
});

View File

@@ -8,20 +8,20 @@
/**
* @type {SoundChannel[]}
*/
let soundChannels = [];
window.soundChannels = [];
// Currently selected sound channel row in the table
let selectedSoundChannel = null;
window.selectedSoundChannel = null;
/**
* Fills the sound channel table body with the provided data.
* @param {SoundChannel[]} vv Sound channel data to populate the table.
*/
function fill_soundchanneltablebody(vv) {
const $tbody = $('#soundchanneltablebody');
const $btnEditSoundChannel = $('#btnEditSoundChannel');
const $tablesizeSoundChannel = $('#tablesizeSoundChannel');
$tbody.empty();
let $btnEditSoundChannel = $('#btnEditSoundChannel');
let $tablesizeSoundChannel = $('#tablesizeSoundChannel');
$('#soundchanneltablebody').empty();
$tablesizeSoundChannel.text('Table Length : N/A');
if (!Array.isArray(vv) || vv.length === 0) return;
@@ -31,9 +31,9 @@ function fill_soundchanneltablebody(vv) {
<td>${item.channel}</td>
<td>${item.ip}</td>
</tr>`;
$tbody.append(row);
$('#soundchanneltablebody').append(row);
let $addedrow = $('#soundchanneltablebody tr:last');
$addedrow.click(function () {
$addedrow.off('click').on('click', function () {
if (selectedSoundChannel) {
selectedSoundChannel.find('td').css('background-color', '');
if (selectedSoundChannel.is($(this))) {
@@ -55,12 +55,12 @@ function fill_soundchanneltablebody(vv) {
* @param {String} APIURL API URL endpoint (default "SoundChannel/")
*/
function reloadSoundChannel(APIURL = "SoundChannel/") {
SoundChannelList = [];
window.soundChannels = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadSoundChannel : ", okdata)
SoundChannelList = okdata;
fill_soundchanneltablebody(SoundChannelList);
window.soundChannels.push(...okdata);
fill_soundchanneltablebody(window.soundChannels);
} else console.log("reloadSoundChannel: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
@@ -82,15 +82,17 @@ $(document).ready(function () {
$btnEditSoundChannel.prop('disabled', true);
let API_SoundChannel = "SoundChannel/";
$findsoundchannel.on('input', function () {
$findsoundchannel.off('input').on('input', function () {
let searchTerm = $(this).val().toLowerCase();
if (searchTerm.length==0){
fill_soundchanneltablebody(SoundChannelList);
window.selectedSoundChannel = null;
fill_soundchanneltablebody(window.soundChannels);
} else {
let filteredChannels = SoundChannelList.filter(channel =>
channel.index.toString().includes(searchTerm) ||
channel.description.toLowerCase().includes(searchTerm) ||
channel.ip.toLowerCase().includes(searchTerm)
window.selectedSoundChannel = null;
let filteredChannels = window.soundChannels.filter(xx =>
xx.index.toString().includes(searchTerm) ||
xx.channel.toLowerCase().includes(searchTerm) ||
xx.ip.toLowerCase().includes(searchTerm)
);
fill_soundchanneltablebody(filteredChannels);
}
@@ -106,7 +108,7 @@ $(document).ready(function () {
}
reloadSoundChannel(API_SoundChannel);
$btnReinitializeSoundChannel.click(() => {
$btnReinitializeSoundChannel.off('click').on('click', () => {
DoClear(API_SoundChannel, "SoundChannels", (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success clear sound channels: " + okdata.message);
@@ -114,7 +116,7 @@ $(document).ready(function () {
alert("Error clear sound channels: " + errdata.message);
});
});
$btnEditSoundChannel.click(() => {
$btnEditSoundChannel.off('click').on('click', () => {
if (selectedSoundChannel) {
let cells = selectedSoundChannel.find('td');
/** @type {SoundChannel} */
@@ -168,11 +170,11 @@ $(document).ready(function () {
}
}
});
$btnExportSoundChannel.click(() => {
$btnExportSoundChannel.off('click').on('click', () => {
DoExport(API_SoundChannel, "soundchannels.xlsx", {});
});
$btnImportSoundChannel.click(() => {
$btnImportSoundChannel.off('click').on('click', () => {
DoImport(API_SoundChannel, (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success import sound channels: " + okdata.message);

View File

@@ -0,0 +1,559 @@
/**
* @typedef {Object} UserDB
* @property {number} index Index number
* @property {string} username Username
* @property {string} password Password (plain)
* @property {string} location Location
* @property {string} airline_tags Airline variable tags (string) separated by semicolon ;
* @property {string} city_tags City variable tags (string) 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
* @type {UserDB[]}
*/
window.userdb = [];
/**
* Currently selected user row in table
* @type {JQuery<HTMLElement>|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
* @param {UserDB[]} vv values to fill
*/
function fill_usertablebody(vv) {
$('#usertablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) {
$('#btnExport').prop('disabled', true);
return;
}
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.username}</td>
<td>${item.location}</td>
<td>${item.airline_tags}</td>
<td>${item.city_tags}</td>
<td>${item.messagebank_ann_id}</td>
<td>${item.broadcastzones}</td>
</tr>`;
$('#usertablebody').append(row);
let $addedrow = $('#usertablebody tr:last');
$addedrow.off('click').on('click', function () {
if (window.selecteduserrow) {
window.selecteduserrow.find('td').css('background-color', '');
if (window.selecteduserrow.is($(this))) {
window.selecteduserrow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
window.selecteduserrow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
$('#btnExport').prop('disabled', false);
}
/**
* Reload UserDB from server with date and filter
* @param {String} APIURL API URL endpoint , default "UserManagement/"
*/
function reloaduserDB(APIURL = "UserManagement/") {
window.userdb = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.userdb.push(...okdata);
fill_usertablebody(window.userdb);
}
}, (errdata) => {
alert("Error loading user database : " + errdata.message);
});
}
$(document).ready(function () {
console.log("usermanagement.js ready");
get_airlinetags();
get_citytags();
get_messagebankids();
get_broadcastzones_descriptions();
let APIURL = "UserManagement/";
function clearAddModal() {
$('#modalindex').val("");
$('#modalusername').val("");
$('#modalpassword').val("");
$('#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
function fill_broadcastzonelist() {
$('#broadcastzonelist').empty();
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
function fill_messagebanklist() {
$('#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>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -19,39 +20,39 @@
<div class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal">
<div class="modal-dialog" role="document">
<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>
</div>
<div class="modal-body">
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><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 class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="broadcastzonedescription" class="form-control input-add" placeholder="description"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="broadcastzonedescription" class="form-control input-add" placeholder="description"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Channel</p>
</div>
<div class="col"><select id="broadcastzonesoundchannel" class="form-control input-add" placeholder="sound channel"></select></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="broadcastzonesoundchannel" class="form-control input-add" placeholder="sound channel"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Box</p>
</div>
<div class="col"><input type="text" id="broadcastzonebox" class="form-control input-add" placeholder="Box ID"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="broadcastzonebox" class="form-control input-add" placeholder="Box ID"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Relay</p>
</div>
<div class="col">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8 pad-relay">
<div class="row">
<div class="col-3">
<div class="row">
@@ -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>
</div>
<div class="col-3">
<div class="col-3 invisible">
<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>
@@ -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>
</div>
<div class="col-3">
<div class="col-3 invisible">
<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>
@@ -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>
</div>
<div class="col-3">
<div class="col-3 invisible">
<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>
@@ -167,9 +168,9 @@
</div>
<div class="row">
<div class="accordion" role="tablist" id="accordion-1">
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="true" aria-controls="accordion-1 .item-1">Sound Channel</button></h2>
<div class="accordion-collapse collapse show item-1" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed bg-heading1" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="false" aria-controls="accordion-1 .item-1">Sound Channel</button></h2>
<div class="accordion-collapse collapse item-1" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
@@ -178,11 +179,11 @@
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="findsoundchannel" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnReinitializeSoundChannel" type="button">Re-Initialize</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-edit" id="btnEditSoundChannel" type="button">Edit</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExportSoundChannel" type="button">Export</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnImportSoundChannel" type="button">Import</button></div>
<div class="row pad-search">
<div class="col-6 col-sm-6 col-md-3 col-lg-3 col-xl-3"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnReinitializeSoundChannel" type="button">Re-Initialize</button></div>
<div class="col-6 col-sm-6 col-md-3 col-lg-3 col-xl-3"><button class="btn w-100 pad-button btn-round-basic color-edit" id="btnEditSoundChannel" type="button">Edit</button></div>
<div class="col-6 col-md-3 col-lg-3 col-xl-3 col-sm--6"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExportSoundChannel" type="button">Export</button></div>
<div class="col-6 col-sm-6 col-md-3 col-lg-3 col-xl-3"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnImportSoundChannel" type="button">Import</button></div>
</div>
<div class="row">
<div class="col">
@@ -194,9 +195,9 @@
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th class="col-sm-3">Description</th>
<th class="col">IP Address</th>
<th class="class05">No</th>
<th class="class75">Description</th>
<th class="class20">IP Address</th>
</tr>
</thead>
<tbody id="soundchanneltablebody"></tbody>
@@ -206,9 +207,9 @@
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="false" aria-controls="accordion-1 .item-2">Broadcast Zones</button></h2>
<div class="accordion-collapse collapse item-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button bg-heading2" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="true" aria-controls="accordion-1 .item-2">Broadcast Zones</button></h2>
<div class="accordion-collapse collapse show item-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
@@ -217,7 +218,7 @@
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="findzone" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="row pad-search">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnClear" type="button">Clear</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnAdd" type="button">Add</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-remove" id="btnRemove" type="button">Remove</button></div>
@@ -235,11 +236,11 @@
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th class="col-sm-2">Description</th>
<th class="col-sm-2">SoundChannel</th>
<th class="col-sm-2">Box</th>
<th class="col">Relay</th>
<th class="class05">No</th>
<th class="class40">Description</th>
<th class="class20">SoundChannel</th>
<th class="class05">ID</th>
<th class="class30">BP</th>
</tr>
</thead>
<tbody id="broadcastzonetablebody"></tbody>
@@ -254,34 +255,35 @@
<div class="modal fade" role="dialog" tabindex="-1" id="soundchannelmodal">
<div class="modal-dialog" role="document">
<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>
</div>
<div class="modal-body">
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4">
<p>Index</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25" type="text" id="soundchannelindex" readonly="" placeholder="index"></div>
<div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-25 form-control input-add" type="text" id="soundchannelindex" readonly="" placeholder="index"></div>
</div>
<div class="row">
<div class="col-4">
<p>Description</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input class="w-100" 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 class="row">
<div class="col-4">
<p>IP Address</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">IP Address</p>
</div>
<div class="col"><input class="w-100" 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 class="modal-footer"><button class="btn btn-light" id="soundchannelclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" 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>
</div>
</div>
</div>
<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/broadcastzones.js"></script>
</body>

View File

@@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS NewGeneration 17092025</title>
<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/styles.css">
</head>
@@ -60,6 +62,10 @@
<path d="M19,7H9C7.9,7,7,7.9,7,9v10c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V9C21,7.9,20.1,7,19,7z M19,9v2H9V9H19z M13,15v-2h2v2H13z M15,17v2h-2v-2H15z M11,15H9v-2h2V15z M17,13h2v2h-2V13z M9,17h2v2H9V17z M17,19v-2h2v2H17z M6,17H5c-1.1,0-2-0.9-2-2V5 c0-1.1,0.9-2,2-2h10c1.1,0,2,0.9,2,2v1h-2V5H5v10h1V17z"></path>
</g>
</svg>&nbsp; &nbsp;Log</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="usermanagement" href="#"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" class="me-2 icon-menu" style="font-size: 20px;">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"></path>
</svg>&nbsp;User Management</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="settinglink" href="#"><svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" class="me-2 icon-menu" style="font-size: 20px;">
<g>
<path d="M0,0h24v24H0V0z" fill="none"></path>
@@ -93,9 +99,10 @@
</div>
<div class="container w-100 pad-container" id="content"></div>
<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/select2.js"></script>
<script src="assets/js/script.js"></script>
<script src="assets/js/select2.min.js"></script>
</body>
</html>

View File

@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -16,7 +17,7 @@
<h2 style="text-align: center;">Language Link</h2>
</div>
</div>
<div class="row">
<div class="row pad-row-search">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<p class="text-add">Search</p>
@@ -41,9 +42,9 @@
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th class="col-sm-2">TAG</th>
<th class="col">Languages</th>
<th class="class05">No</th>
<th class="class20">TAG</th>
<th class="class75">Languages</th>
</tr>
</thead>
<tbody id="languagebanktablebody"></tbody>
@@ -53,27 +54,27 @@
<div class="modal fade" role="dialog" tabindex="-1" id="languagemodal">
<div class="modal-dialog" role="document">
<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>
</div>
<div class="modal-body">
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 form-control input-add" type="text" id="languagelinkindex" readonly=""></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="languagelinkindex" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Tag</p>
</div>
<div class="col"><input type="text" id="languagelinktag" class="form-control input-add"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="languagelinktag" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<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 text-add">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8 text-add">
<div class="form-check"><input class="form-check-input" type="checkbox" id="langId" name="languages[]" value="id"><label class="form-check-label" for="langId">Indonesia</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langLocal" name="languages[]" value="id"><label class="form-check-label" for="langId-1">Local</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langEn" name="languages[]" value="en"><label class="form-check-label" for="langEn">English</label></div>
@@ -88,6 +89,7 @@
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/languagelink.js"></script>
</body>

View File

@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -17,15 +18,16 @@
</div>
</div>
<div class="row">
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2">
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2">
<p class="text-add">Select Log Date</p>
</div>
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2"><input id="logdate" class="form-control" type="date"></div>
<div class="col-md-4 col-lg-4 col-xl-4"><button class="btn btn-primary w-100 h-100" id="btnExport" type="button">Export</button></div>
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2">
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2"><input id="logdate" class="form-control" type="date"></div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExport" type="button">Export</button></div>
<div class="col-md-2 col-lg-2 col-xl-2"></div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2">
<p class="text-add">Search</p>
</div>
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2"><input type="text" id="searchfilter" class="form-control" placeholder="Search Filter"></div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2"><input type="text" id="searchfilter" class="form-control" placeholder="Search Filter"></div>
</div>
<div class="row">
<div class="col">
@@ -37,11 +39,11 @@
<table class="table">
<thead>
<tr>
<th class="col-1 col-md-1">No</th>
<th class="col-2 col-md-2 col-lg-2">Date</th>
<th class="col-2 col-md-2 col-lg-2">Time</th>
<th class="col-2 col-md-2 col-lg-2">Machine</th>
<th>Description</th>
<th class="class10">No</th>
<th class="class15">Date</th>
<th class="class15">Time</th>
<th class="class15">Machine</th>
<th class="class45">Description</th>
</tr>
</thead>
<tbody id="logtablebody"></tbody>
@@ -49,6 +51,7 @@
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/log.js"></script>
</body>

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS NewGeneration 17092025</title>
<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/styles.css">
</head>
@@ -13,22 +14,21 @@
<body>
<section class="position-relative py-4 py-xl-5">
<div class="container">
<div class="row mb-5">
<div class="col-md-8 col-xl-6 text-center mx-auto">
<h2>Sign In</h2>
</div>
</div>
<div class="row d-flex justify-content-center">
<div class="row mb-4"></div>
<div class="row d-flex justify-content-center mb-7">
<div class="col-md-6 col-xl-4">
<div class="card mb-5 card-login">
<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>
</svg></div>
<form class="text-center" method="post">
<div class="mb-3"><input class="form-control input-login" type="text" name="username" placeholder="Username"></div>
<div class="mb-3"><input class="form-control input-login" type="password" name="password" placeholder="Password"></div>
<div class="mb-3"><button class="btn btn-primary d-block w-100 btn-login" type="submit">Sign In</button></div>
<h2 class="mb-3 h-login">Login</h2>
<form class="text-center py-2 bottom-signin" method="post">
<p class="p-login">Username</p>
<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>
</div>
</div>
@@ -37,6 +37,7 @@
</div>
</section>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
</body>
</html>

View File

@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -16,12 +17,12 @@
<h2 style="text-align: center;">Message Bank</h2>
</div>
</div>
<div class="row">
<div class="row pad-row-search">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<p class="text-add">Search</p>
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="findmessage" placeholder="Search keyword" name="findsoundbank"></div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control pad-search" type="text" id="findmessage" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnClear" type="button">Clear</button></div>
@@ -41,13 +42,13 @@
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th class="col-sm-2">Description</th>
<th class="col-sm-1">Language</th>
<th class="col-sm-1">ANN ID</th>
<th class="col-sm-1">Type</th>
<th class="col-sm-3">Message Details</th>
<th class="col-sm-3">Message Tags</th>
<th class="class05">No</th>
<th class="class15">Description</th>
<th class="class10">Language</th>
<th class="class10">ANN ID</th>
<th class="class10">Type</th>
<th class="class35">Message Details</th>
<th class="class15">Message Tags</th>
</tr>
</thead>
<tbody id="messagebanktablebody"></tbody>
@@ -55,29 +56,29 @@
</div>
</div>
<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-header">
<div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Message</h4>
</div>
<div class="modal-body">
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Index</p>
</div>
<div class="col"><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 class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Description</p>
</div>
<div class="col"><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 class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Language</p>
</div>
<div class="col"><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="LOCAL">Local</option>
<option value="ENGLISH">English</option>
@@ -87,40 +88,40 @@
</select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">ANN ID</p>
</div>
<div class="col"><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 class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Voice Type</p>
</div>
<div class="col"><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_2">Voice 2</option>
<option value="VOICE_3">Voice 3</option>
</select></div>
</div>
<div class="row">
<div class="col">
<div class="col bg-light">
<ul class="list-unstyled w-100 h-100" id="messageavailablevariables"></ul>
</div>
<div class="col-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="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2">
<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. -->
<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>
<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. -->
<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>
<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. -->
<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>
</div>
<div class="col">
<div class="col bg-light">
<ul class="list-unstyled w-100 h-100" id="messageselectedvariables"></ul>
</div>
</div>
@@ -130,6 +131,7 @@
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/messagebank.js"></script>
</body>

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -16,7 +17,70 @@
<h2 style="text-align: center;">Setting</h2>
</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/js/bs-init.js"></script>
<script src="assets/js/dragdrop.js"></script>
</body>
</html>

View File

@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -41,13 +42,13 @@
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th>Description</th>
<th class="col-sm-1">TAG</th>
<th class="col-sm-1">Category</th>
<th class="col-sm-1">Language</th>
<th class="col-sm-1">Type</th>
<th class="col-sm-3">Filename</th>
<th class="class05">No</th>
<th class="class20">Description</th>
<th class="class10">TAG</th>
<th class="class15">Category</th>
<th class="class15">Language</th>
<th class="class10">Type</th>
<th class="class25">Filename</th>
</tr>
</thead>
<tbody id="soundbanktablebody"></tbody>
@@ -57,51 +58,51 @@
<div class="modal fade border-0" role="dialog" tabindex="-1" id="soundbankmodal">
<div class="modal-dialog" role="document">
<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>
</div>
<div class="modal-body">
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 input-add form-control" type="text" id="modalindex" readonly=""></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="modalindex" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="modaldescription" class="form-control input-add"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modaldescription" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">TAG</p>
</div>
<div class="col"><input type="text" id="modaltag" class="form-control input-add"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modaltag" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Category</p>
</div>
<div class="col"><select id="modalcategory" 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="modalcategory" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<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"><select id="modallanguage" 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="modallanguage" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Voice Type</p>
</div>
<div class="col"><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 class="row" style="height: 100px;">
<div class="col-4 col-sm-3">
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Path</p>
</div>
<div class="col"><select class="w-100 js-example-basic-single" 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 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>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/soundbank.js"></script>
</body>

View File

@@ -4,21 +4,22 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
<body>
<div class="card" id="streamercard">
<div class="card-body">
<div class="card-body card-channel">
<h4 class="card-title" id="streamertitle">Channel 01</h4>
<div class="row">
<div class="col-8">
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
<h6 class="text-muted mb-2" id="streamerip">IP :&nbsp;192.168.10.10</h6>
</div>
<div class="col">
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
<h6 class="text-muted mb-2" id="streamerbuffer">Free : 64KB</h6>
</div>
</div>
@@ -29,6 +30,7 @@
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
</body>
</html>

View File

@@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
@@ -16,7 +17,7 @@
<h2 style="text-align: center;">Schedule Bank</h2>
</div>
</div>
<div class="row">
<div class="row pad-row-search">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<p class="text-add">Search</p>
@@ -41,14 +42,15 @@
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th class="col-sm-2">Description</th>
<th class="col-sm-1">Day</th>
<th class="col-sm-1">Time</th>
<th class="col-sm-2">Sound Path</th>
<th class="col-sm-1">Repeat</th>
<th class="col-sm-1">Enable</th>
<th>Broadcast Zones</th>
<th class="class05">No</th>
<th class="class15">Description</th>
<th class="class15">Day</th>
<th class="class10">Time</th>
<th class="class15">Sound Path</th>
<th class="class10">Repeat</th>
<th class="class05">Enable</th>
<th class="class15">Broadcast Zones</th>
<th class="class10">Language</th>
</tr>
</thead>
<tbody id="schedulebanktablebody"></tbody>
@@ -58,115 +60,113 @@
<div class="modal fade" role="dialog" tabindex="-1" id="schedulemodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Schedule</h4>
</div>
<div class="modal-body">
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 input-add form-control" type="text" id="scheduleid" readonly=""></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="scheduleid" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="scheduledescription" class="input-add form-control"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Day</p>
</div>
<div class="col">
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="scheduleeveryday" name="dayselection" value="Everyday"><label class="form-check-label" for="formCheck-1">Everyday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<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">
<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 class="row">
<div class="col">
<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>
</div>
</div>
<div class="row">
<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">
<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">
<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">
<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">
<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 class="row">
<div class="col">
<div class="col-7 col-sm-7 col-md-7 col-lg-6 col-xl-6 pad-day">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulespecialdate" name="dayselection"><label class="form-check-label" for="formCheck-9">Special Date</label></div>
</div>
<div class="col"><input id="scheduledate" type="date"></div>
<div class="col-sm-5 col-md-5 col-lg-6 col-xl-6"><input id="scheduledate" class="form-control" type="date"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Time</p>
</div>
<div class="col">
<div class="row w-100 h-100">
<div class="col-3"><input class="w-100 h-100" type="number" id="schedulehour" value="0" min="0" max="23" step="1"></div>
<div class="col-1">
<p class="w-100 h-100">(H)</p>
<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-2 col-xl-2">
<p class="pad-time">(H)</p>
</div>
<div class="col-3"><input class="w-100 h-100" type="number" id="scheduleminute" value="0" min="0" max="59" step="1"></div>
<div class="col-1">
<p class="w-100 h-100">(M)</p>
<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-2 col-xl-2">
<p class="pad-time">(M)</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<p class="text-add">Sound Path</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Message</p>
</div>
<div class="col"><input type="text" id="schedulesoundpath" class="input-add form-control"></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-3">
<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="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Repeat</p>
</div>
<div class="col"><input class="w-25 form-select input-add" type="number" id="schedulerepeat" min="0" max="5" step="1" value="0"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 form-select input-add" type="number" id="schedulerepeat" min="0" max="5" step="1" value="0"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Enable</p>
</div>
<div class="col"><input type="checkbox" id="scheduleenable" class="form-check-input form-check text-add" checked=""></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="checkbox" id="scheduleenable" class="form-check-input form-check text-add" checked=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Broadcast Zones</p>
</div>
<div class="col border p-2" 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 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>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/schedulebank.js"></script>
</body>

View File

@@ -0,0 +1,205 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_17OKT25</title>
<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/styles.css">
</head>
<body class="bg-body">
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">User Management</h2>
</div>
</div>
<div class="row">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<p class="text-add">Search</p>
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="finduser" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="col">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnClear" type="button">Clear</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnAdd" type="button">Add</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-remove" id="btnRemove" type="button">Remove</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-edit" id="btnEdit" type="button">Edit</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExport" type="button">Export</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnImport" type="button">Import</button></div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="class05">No</th>
<th class="class10">Username</th>
<th class="class15">Location</th>
<th class="class15">Airline</th>
<th class="class15">City</th>
<th class="class20">Messagebank</th>
<th class="class20">Broadcast Zones</th>
</tr>
</thead>
<tbody id="usertablebody"></tbody>
</table>
</div>
</div>
<div class="modal fade border-0" role="dialog" tabindex="-1" id="addmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header text-start show bg-header-modal">
<h4 class="modal-title text-center align-content-center">Add / Edit User</h4>
</div>
<div class="modal-body bg-modal-body">
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</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="modalindex" readonly=""></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">Username</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalusername" class="form-control input-add"></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">Password</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="password" id="modalpassword" class="form-control input-add"></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">Verify Password</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="password" id="modalverifypassword" 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">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="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Message Bank</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="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 class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Broadcast Zones</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 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 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>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="soundbankmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header form-control input-add">
<h4 class="modal-title">Sound Bank Selection</h4>
</div>
<div class="modal-body bg-modal-body">
<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 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 class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header form-control input-add">
<h4 class="modal-title">Broadcast Zones Selection</h4>
</div>
<div class="modal-body bg-modal-body">
<ul id="broadcastzonelist"></ul>
</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 class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header form-control input-add">
<h4 class="modal-title">Message Bank Selection</h4>
</div>
<div class="modal-body bg-modal-body">
<ul id="messagebanklist"></ul>
</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>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/usermanagement.js"></script>
</body>
</html>

View File

@@ -1,8 +1,13 @@
import audio.AudioPlayer
import audio.ContentCache
import audio.TCPReceiver
import audio.UDPReceiver
import barix.BarixConnection
import barix.TCP_Barix_Command_Server
import codes.Somecodes
import com.sun.jna.Platform
import commandServer.TCP_Android_Command_Server
import content.Category
import content.Language
import content.VoiceType
import database.Log
@@ -13,14 +18,22 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.tinylog.Logger
import org.tinylog.provider.ProviderRegistry
import oshi.util.GlobalConfig
import web.WebApp
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.concurrent.fixedRateTimer
import kotlin.io.path.absolutePathString
lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
const val version = "0.0.2 (23/09/2025)"
lateinit var udpreceiver: UDPReceiver
lateinit var tcpreceiver: TCPReceiver
const val version = "0.0.8 (16/10/2025)"
// AAS 64 channels
const val max_channel = 64
// dipakai untuk pilih voice type, bisa diganti via web nanti
var selected_voice = VoiceType.VOICE_1.name
@@ -35,6 +48,52 @@ val urutan_bahasa = listOf(
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
fun main() {
if (Platform.isWindows()) {
@@ -42,8 +101,14 @@ fun main() {
GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true)
}
Logger.info { "Starting AAS New Generation version $version" }
folder_preparation()
audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate
audioPlayer.InitAudio(1)
files_preparation()
db = MariaDB()
val subcode01 = MainExtension01()
@@ -54,8 +119,12 @@ fun main() {
delay(1000)
// prioritas 1 , habisin queue paging
subcode01.Read_Queue_Paging()
// prioritas 2, habisin queue table
subcode01.Read_Queue_Table()
// prioritas 2, habisin queue shalat
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
@@ -74,27 +143,47 @@ fun main() {
))
web.Start()
udpreceiver = UDPReceiver()
if (udpreceiver.Start()) {
Logger.info { "UDP Receiver started on port 5002" }
} else {
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()
androidserver.StartTcpServer(5003){
Logger.info { it }
db.logDB.Add(Log.NewLog("ANDROID", it))
}
val barixserver = TCP_Barix_Command_Server()
barixserver.StartTcpServer { cmd ->
Logger.info { cmd }
val _tcp = barixserver.getSocket(cmd.ipaddress)
val _streamer = StreamerOutputs[cmd.ipaddress]
val _sc = db.soundchannelDB.List.find { it.ip == cmd.ipaddress }
if (_streamer == null) {
// 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) {
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.statusData = cmd.statusdata
_bc.commandsocket = _tcp
StreamerOutputs[cmd.ipaddress] = _bc
Logger.info { "Created new Streamer Output for channel ${_sc.channel} with IP ${cmd.ipaddress}" }
}
} else {
@@ -102,9 +191,25 @@ fun main() {
if (_sc != null && _sc.channel != _streamer.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.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
}
}
}
}
@@ -116,16 +221,22 @@ fun main() {
}
}
db.Add_Log("AAS"," Application started")
// shutdown hook
Runtime.getRuntime().addShutdownHook(Thread {
db.Add_Log("AAS"," Application stopping")
Logger.info { "Shutdown hook called, stopping services..." }
barixserver.StopTcpCommand()
androidserver.StopTcpCommand()
onlinechecker.cancel()
web.Stop()
udpreceiver.Stop()
tcpreceiver.Stop()
audioPlayer.Close()
db.close()
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
import audio.Bass.BASS_ACTIVE_PLAYING
import audio.Bass.BASS_DEVICE_ENABLED
import audio.Bass.BASS_DEVICE_INIT
import audio.Bass.BASS_POS_BYTE
import audio.Bass.BASS_STREAM_DECODE
import audio.Bass.BASS_SAMPLE_MONO
import audio.Bass.STREAMPROC_PUSH
import audio.BassEnc.BASS_ENCODE_PCM
import codes.Result_Boolean_String
import codes.Somecodes.Companion.ValidFile
import codes.Somecodes.Companion.ValidString
import com.sun.jna.Memory
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import com.sun.jna.Pointer
import contentCache
import org.tinylog.Logger
import java.util.function.BiConsumer
@Suppress("unused")
class AudioPlayer (var samplingrate: Int) {
@@ -126,73 +123,183 @@ class AudioPlayer (var samplingrate: Int) {
return result
}
/**
* Writes the audio data from a byte array to a WAV file.
* @param data The byte array containing the audio data.
* @param target The target file name for the WAV file.
* @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(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.
* @param sources List of AudioFileInfo objects containing the audio data to write.
* @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>) {
if (sources.isEmpty() || !ValidFile(target)) {
callback.accept(false, " Invalid sources")
return
fun WavWriter(sources: List<AudioFileInfo>, target: String, withChime: Boolean = true) : Result_Boolean_String {
if (sources.isEmpty()) {
return Result_Boolean_String(false,"Invalid Source")
}
if (!ValidString(target)) {
callback.accept(false, " Invalid target file name")
return
return Result_Boolean_String(false, " Invalid target file name")
}
val job = CoroutineScope(Dispatchers.Default)
job.launch(CoroutineName("WavWriter $target")) {
bass.BASS_SetDevice(0) // Set to No Sound device for writing
val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, STREAMPROC_PUSH, null)
val streamhandle = bass.BASS_StreamCreate(samplingrate, 1, BASS_STREAM_DECODE, Pointer(-1), null)
if (streamhandle==0){
callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
return@launch
return Result_Boolean_String(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
}
val encodehandle = bassenc.BASS_Encode_Start(streamhandle, target, BASS_ENCODE_PCM, null, null)
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
return Result_Boolean_String(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
}
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 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
val mem = Memory(source.bytes.size.toLong())
mem.write(0, source.bytes, 0, source.bytes.size)
val pushresult = bass.BASS_StreamPutData(streamhandle, mem, source.bytes.size)
if (pushresult == -1) {
Logger.error { "Failed to write data from ${source.fileName} to stream: ${bass.BASS_ErrorGetCode()}" }
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
} else {
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)
if (allsuccess){
callback.accept(true, "WAV file written successfully: $target")
return if (allsuccess){
Result_Boolean_String(true, "WAV file written successfully: $target")
} else {
callback.accept(false, "Failed to write some data to WAV file: $target")
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);
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(Pointer file, long offset, long length, int flags);
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_FXSetPriority(int handle, int priority);
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()
}
}
}

79
src/audio/UDPReceiver.kt Normal file
View File

@@ -0,0 +1,79 @@
package audio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.DatagramSocket
import java.util.function.Consumer
@Suppress("unused")
/**
* UDPReceiver is a class that listens for UDP packets on a specified port.
* It is designed to run in a separate thread and can be stopped when no longer needed.
* @param portnumber The port to listen for incoming UDP packets (default is 5002
*/
class UDPReceiver(val portnumber: Int = 5002) {
private lateinit var socket : DatagramSocket
private var isRunning = false
private val dataCallback = mutableMapOf<String, Consumer<ByteArray>>()
/**
* Start listening for UDP packets on the specified port.
* @return true if successful, false otherwise
*/
fun Start() : Boolean{
return try {
socket = DatagramSocket(portnumber)
isRunning = true
CoroutineScope(Dispatchers.IO).launch {
while(isRunning){
try {
val buffer = ByteArray(2048)
val packet = java.net.DatagramPacket(buffer, buffer.size)
socket.receive(packet)
val data = ByteArray(packet.length)
System.arraycopy(packet.data, 0, data, 0, packet.length)
dataCallback[packet.address.hostAddress].let {
it?.accept(data)
}
} catch (e: Exception) {
if (isRunning) {
println("Error receiving UDP packet: ${e.message}")
}
}
}
}
true
} catch (e: Exception) {
false
}
}
/**
* Register a callback function to be called when data is received from the specified IP address.
* @param ipaddress The IP address to listen for incoming UDP packets.
* @param callback A callback function that will be called when data is received from the specified IP address.
*/
fun RequestDataFrom(ipaddress: String, callback: Consumer<ByteArray>){
dataCallback[ipaddress] = callback
}
/**
* Unregister the callback function for the specified IP address.
* @param ipaddress The IP address to stop listening for incoming UDP packets.
*/
fun StopRequestDataFrom(ipaddress: String){
dataCallback.remove(ipaddress)
}
/**
* Stop listening for UDP packets and close the socket.
*/
fun Stop(){
if (isRunning){
isRunning = false
socket.close()
}
}
}

View File

@@ -16,6 +16,7 @@ import java.net.InetSocketAddress
import java.util.function.BiConsumer
import java.util.function.Consumer
@Deprecated("Sepertinya gak jadi pake")
@Suppress("unused")
/**
* UDPReceiverToFile is a class that listens for UDP packets on a specified address and port

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Deprecated("Sepertinya gak jadi pake ini")
@Suppress("unused")
class UDPSenderFromFile(val fileName: String, val bytesPerPackage: Int=1024, targetIP: Array<String>, targetPort: Int ) {
val bass: Bass = Bass.Instance

View File

@@ -6,9 +6,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
import java.net.Socket
import java.nio.ByteBuffer
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 _vu: Int = 0
private var _onlinecounter = 0
private val udp = DatagramSocket(0)
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
}
/**
* 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
*/
@@ -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>) {
if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
DatagramSocket().use{ udp ->
val bb = ByteBuffer.wrap(data)
while(bb.hasRemaining()){
try {
val chunk = ByteArray(if (bb.remaining() > 1400) 1400 else bb.remaining())
val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining())
bb.get(chunk)
//println("Buffer remain: $bufferRemain, sending chunk size: ${chunk.size}")
while(bufferRemain<chunk.size){
delay(10)
//println("Waiting until buffer enough..")
}
udp.send(DatagramPacket(chunk, chunk.size, inet))
delay(5)
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")
}
}
}
} 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 {
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,17 +3,19 @@ package barix
import codes.Somecodes.Companion.ValidString
import kotlinx.coroutines.*
import org.tinylog.Logger
import java.io.DataInputStream
import java.net.ServerSocket
import java.net.Socket
import java.nio.ByteBuffer
import java.util.function.Consumer
@Suppress("unused")
class TCP_Barix_Command_Server {
private var tcpserver: ServerSocket? = null
private var job: Job? = null
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
private val regex = """\$\\"STATUSBARIX;(\d+);(\d+);?(\d)?\\"$"""
private val regex = """STATUSBARIX;(\d+);(\d+);?(\d)?"""
private val pattern = Regex(regex)
/**
@@ -27,25 +29,29 @@ class TCP_Barix_Command_Server {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
Logger.info { "TCP StreamerOutput server started on port $port" }
while (isActive) {
if (tcpserver?.isClosed == true) break
if (tcpserver.isClosed) break
try {
tcpserver?.accept().use { socket ->
{
CoroutineScope(Dispatchers.Main).launch {
if (socket != null) {
val key : String = socket.inetAddress.hostAddress+":"+socket.port
val socket = tcpserver.accept()
CoroutineScope(Dispatchers.IO).launch {
val key : String = socket.inetAddress.hostAddress
socketMap[key] = socket
Logger.info { "Start communicating with $key" }
socket.getInputStream().use { din ->
{
Logger.info { "Start communicating with Streamer Output with IP : $key" }
try{
val din = DataInputStream(socket.getInputStream())
while (isActive) {
if (din.available()>0){
val bb = ByteArray(din.available())
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
val str = String(bb)
//println("Received from $key : $str")
if (ValidString(str)) {
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
pattern.find(str)?.let { matchResult ->
@@ -56,22 +62,21 @@ class TCP_Barix_Command_Server {
buffremain.toInt(),
statusdata.toIntOrNull() ?: 0
)
Logger.info { "Received valid command from $key : $status" }
//Logger.info { "Received valid command from $key : $status" }
cb.accept(status)
} ?: run {
Logger.warn { "Invalid command format from $key : $str" }
}
}
}
}
}
}
Logger.info { "Finished communicating with $key" }
socketMap.remove(key)
}
}
} 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" }
socketMap.remove(key)
}
} catch (ex: Exception) {
@@ -94,21 +99,30 @@ class TCP_Barix_Command_Server {
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver?.close()
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job?.join()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
} finally {
tcpserver = null
}
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.module.kotlin.jacksonObjectMapper
import content.Category
import content.Language
import content.NetworkInformation
import content.ScheduleDay
import content.VoiceType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.Logger
import oshi.SystemInfo
import oshi.hardware.CentralProcessor
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.Path
import java.time.LocalDateTime
@@ -22,11 +30,18 @@ import kotlin.io.path.name
class Somecodes {
companion object {
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 PagingResult_directory : Path = Path.of(current_directory,"PagingResult")
val si = SystemInfo()
val processor: CentralProcessor = si.hardware.processor
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 dateformat1: 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 []
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.
*/
@@ -110,14 +177,13 @@ class Somecodes {
/**
* List all audio files (.mp3 and .wav) in the specified directory and its subdirectories.
* 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.
*/
fun ListAudioFiles(dir: String = current_directory) : List<String>{
fun ListAudioFiles(p: Path) : List<String>{
return try{
// find all files that ends with .mp3 or .wav
// and find them recursively
val p = Path.of(dir)
if (Files.exists(p) && Files.isDirectory(p)){
Files.walk(p)
// cari file regular saja
@@ -218,6 +284,45 @@ class Somecodes {
, (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.
* @param value The value to check.
@@ -227,6 +332,23 @@ class Somecodes {
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.
* @param value The string to check.
@@ -374,6 +496,35 @@ class Somecodes {
sb.append(".wav")
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

@@ -0,0 +1,48 @@
package commandServer
import codes.Somecodes.Companion.PagingResult_directory
import codes.Somecodes.Companion.filenameformat
import java.io.ByteArrayOutputStream
import java.nio.file.Path
import java.time.LocalDateTime
/**
* Class to handle a paging job, storing incoming audio data and metadata.
* @param fromIP The IP address from which the paging data is received.
* @param broadcastzones The zones to which the paging is broadcasted, is a semicolon-separated string.
*/
class PagingJob(val fromIP: String, val broadcastzones: String) {
val filePath : Path = PagingResult_directory.resolve("PAGING_"+fromIP+"_"+LocalDateTime.now().format(filenameformat)+".wav")
private val bos : ByteArrayOutputStream = ByteArrayOutputStream()
var totalBytesReceived = 0; private set
var isRunning = true; private set
/**
* Expected Size from PCMFILE android
*/
var expectedSize = 0
/**
* Adds incoming audio data to the job.
* @param data The byte array containing audio data.
* @param length The number of bytes to write from the data array.
*/
fun addData(data: ByteArray, length: Int) {
bos.write(data, 0, length)
totalBytesReceived += length
}
/**
* Retrieves the accumulated audio data as a byte array.
* @return A byte array containing all received audio data.
*/
fun GetData(): ByteArray {
return bos.toByteArray()
}
fun Close(){
bos.close()
isRunning = false
}
}

View File

@@ -1,6 +1,14 @@
package commandServer
import audioPlayer
import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.datetimeformat1
import content.Category
import content.Language
import database.Messagebank
import database.QueuePaging
import database.QueueTable
import database.Soundbank
import db
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -9,19 +17,24 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tinylog.Logger
import tcpreceiver
import udpreceiver
import java.net.ServerSocket
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.LocalDateTime
import java.util.function.Consumer
import kotlin.io.path.absolutePathString
@Suppress("unused")
class TCP_Android_Command_Server {
private var tcpserver: ServerSocket? = null
private var job: Job? = null
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
lateinit var logcb: Consumer<String>
private val listUserLogin = mutableListOf<userLogin>()
private val listOnGoingPaging = mutableMapOf<String, PagingJob>()
/**
* Start TCP Command Server
@@ -35,46 +48,50 @@ class TCP_Android_Command_Server {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
Logger.info { "TCP Android server started on port $port" }
while (isActive) {
if (tcpserver?.isClosed == true) break
if (tcpserver.isClosed) break
try {
tcpserver?.accept().use { socket ->
{
CoroutineScope(Dispatchers.Main).launch {
val socket = tcpserver.accept()
CoroutineScope(Dispatchers.IO).launch {
if (socket != null) {
val key: String = socket.inetAddress.hostAddress + ":" + socket.port
// key is IP address only
val key: String = socket.inetAddress.hostAddress
socketMap[key] = socket
Logger.info { "Start communicating with $key" }
socket.getInputStream().let { din ->
{
Logger.info { "Start communicating with IPMT/IPM with IP $key" }
val din = socket.getInputStream()
val dout = socket.getOutputStream()
try{
while (isActive) {
if (din.available() > 0) {
val bb = ByteArray(din.available())
din.read(bb)
// B4A format, 4 bytes di depan adalah size
val str = String(bb, 4, bb.size - 4)
//println("Received command from $key : $str")
str.split("@").map { it.trim() }.filter { ValidString(it) }
.map { it.uppercase() }.forEach {
.forEach {
process_command(key,it) { reply ->
try {
socket.getOutputStream().write(String_to_Byte_Android(reply))
dout.write(String_to_Byte_Android(reply))
} catch (e: Exception) {
logcb.accept("Failed to send reply to $key, Message : ${e.message}")
}
logcb.accept("Failed to send reply to $key, Message : $e")
}
}
}
}
}
} 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) {
logcb.accept("Failed accepting TCP Socket, Message : ${ex.message}")
@@ -90,6 +107,14 @@ class TCP_Android_Command_Server {
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
* @param str The input string
@@ -97,8 +122,8 @@ class TCP_Android_Command_Server {
*/
private fun String_to_Byte_Android(str: String): ByteArray {
if (ValidString(str)) {
val len = str.length
val bytes = str.toByteArray()
val bytes = str.toByteArray(Charsets.UTF_8)
val len = bytes.size
return ByteBuffer.allocate(len + 4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(len)
@@ -108,23 +133,32 @@ class TCP_Android_Command_Server {
return ByteArray(0)
}
/**
* Process command from Android client
* @param key The client IP address
* @param cmd The command string
* @param cb Callback to send reply string
*/
private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
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]) {
"GETLOGIN" -> {
// Android login request
val username = parts.getOrElse(1) { "" }
val password = parts.getOrElse(2) { "" }
if (ValidString(username) && ValidString(password)) {
if (db.userDB.List.any{it.username==username && it.password==password}) {
cb.accept("LOGIN;TRUE@")
if (db.userDB.List.any{
it.username==username && it.password==password}) {
val existing = listUserLogin.find { it.ip == key}
if (existing!=null){
existing.username = username
} else{
listUserLogin.add(userLogin(key, username))
}
cb.accept("LOGIN;TRUE@")
logcb.accept("Android Login success from $key as $username")
return
} else {
logcb.accept("Android Login failed from $key as $username")
cb.accept("LOGIN;FALSE@")
@@ -136,31 +170,476 @@ class TCP_Android_Command_Server {
}
"PCMFILE_START" ->{
// TODO read coding here
}
// 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(",",";")
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" -> {
// TODO read coding here
// 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" -> {
// TODO read coding here
// Start Paging request from IPM
val zones = parts.getOrElse(1) { "" }.replace(",",";")
if (ValidString(zones)){
// create pagingjob
val pj = PagingJob(key, zones)
// masukin ke list
listOnGoingPaging[key] = pj
Logger.info{"PagingJob created for IPM $key, zones: $zones, file: ${pj.filePath.absolutePathString()}"}
// start minta data dari udpreceiver
udpreceiver.RequestDataFrom(key){
// push data ke paging job
pj.addData(it, it.size)
}
logcb.accept("Paging started from IPM $key")
cb.accept("STARTPAGINGAND;OK@")
return
} else logcb.accept("Paging start from IPM $key failed, empty zones")
cb.accept("STARTPAGINGAND;NG@")
}
"STOPPAGINGAND" -> {
// TODO read coding here
// stop paging request from IPM
val pj = listOnGoingPaging[key]
if (pj!=null){
listOnGoingPaging.remove(key)
udpreceiver.StopRequestDataFrom(key)
logcb.accept("Paging stopped from IPM $key")
// get remaining data
val data = pj.GetData()
pj.Close()
Logger.info{"Paging job closed from IPM $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),
"IPM",
"PAGING",
pj.filePath.absolutePathString(),
pj.broadcastzones
)
if (db.queuepagingDB.Add(qp)){
db.queuepagingDB.Resort()
logcb.accept("Paging audio inserted to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}")
cb.accept("STOPPAGINGAND;OK@")
return
} else logcb.accept("Failed to insert paging audio to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}")
} 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@")
}
"CANCELPAGINGAND" -> {
// TODO read coding here
// cancel paging request from IPM
val pj = listOnGoingPaging[key]
if (pj!=null){
pj.Close()
listOnGoingPaging.remove(key)
udpreceiver.StopRequestDataFrom(key)
logcb.accept("Paging from IPM $key cancelled")
cb.accept("CANCELPAGINGAND;OK@")
return
} else logcb.accept("Paging cancel from IPM $key failed, no ongoing paging")
cb.accept("CANCELPAGINGAND;NG@")
}
"STARTINITIALIZE" -> {
// TODO read coding here
val username = parts.getOrElse(1) { "" }
if (ValidString(username)){
val userlogin = listUserLogin.find { it.username == username }
if (userlogin != null){
val userdb = db.userDB.List.find { it.username == username }
if (userdb != null){
//println("Sending initialization data to $key with username $username")
val result = StringBuilder()
// kirim Zone
result.append("ZONE")
userdb.broadcastzones.split(";").map { it.trim() }.filter { it.isNotBlank() }.forEach {
result.append(";")
result.append(it)
}
result.append("@")
cb.accept(result.toString())
// kirim MSGTOTAL
result.clear()
val VARMESSAGES = mutableListOf<Messagebank>()
result.append("MSGTOTAL;")
userdb.messagebank_ann_id
// messagebank_ann_id adalah rentengan ANN_ID (digit) yang dipisah dengan ;
.split(";")
// trim dulu
.map { it.trim() }
// bukan string kosong antar dua tanda ;
.filter { it.isNotBlank() }
// iterasi setiap ANN_ID
.forEach { annid ->
// masukin ke VARMESSAGES yang unik secara ANN_ID dan Language
val xx = db.messageDB.List
.filter{ it.ANN_ID == annid.toUInt() }
.distinctBy { it.ANN_ID }
VARMESSAGES.addAll(xx)
}
result.append(VARMESSAGES.size).append("@")
cb.accept(result.toString())
// kirim VARAPTOTAL
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
if (VARMESSAGES.isNotEmpty()) {
result.clear()
VARMESSAGES.forEachIndexed { index, msg ->
val ann_id = msg.ANN_ID
val msg_indo = db.messageDB.List.find {
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
if (VARAPTOTAL.isNotEmpty()) {
result.clear()
VARAPTOTAL.forEachIndexed { index, sb ->
result.append("VARAP;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARCITY
if (VARCITYTOTAL.isNotEmpty()) {
result.clear()
VARCITYTOTAL.forEachIndexed { index, sb ->
result.append("VARCITY;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARPLACES
if (VARPLACESTOTAL.isNotEmpty()) {
result.clear()
VARPLACESTOTAL.forEachIndexed { index, sb ->
result.append("VARPLACES;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// append VARSHALAT
if (VARSHALATTOTAL.isNotEmpty()) {
result.clear()
VARSHALATTOTAL.forEachIndexed { index, sb ->
result.append("VARSHALAT;$index;${sb.TAG};${sb.Description}@")
}
cb.accept(result.toString())
}
// 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
} 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 empty username")
cb.accept("STARTINITIALIZE;FALSE@")
}
"BROADCASTAND" -> {
// TODO read coding here
// semi auto dari android, masukin ke queue table
val desc = parts.getOrElse(1) { "" }
// language bisa lebih dari satu, dipisah dengan koma
val lang = parts.getOrElse(2) { "" }.replace(",",";")
// tags bisa lebih dari satu, dipisah dengan spasi
val tags = parts.getOrElse(3) { "" }.replace(",",";")
// zone bisa lebih dari satu, dipisah dengan koma
val zone = parts.getOrElse(4) { "" }.replace(",",";")
if (ValidString(desc)){
if (ValidString(lang)){
if (ValidString(tags)){
if (ValidString(zone)){
val qt = QueueTable(
0u,
LocalDateTime.now().format(datetimeformat1),
"ANDROID",
"SOUNDBANK",
desc,
tags,
zone,
1u,
lang
)
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")
cb.accept("BROADCASTAND;OK@")
return
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, cannot add to queue table")
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty zone")
} 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 description")
cb.accept("BROADCASTAND;NG@")
}
else -> {
@@ -176,20 +655,18 @@ class TCP_Android_Command_Server {
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver?.close()
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job?.join()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
} finally {
tcpserver = null
}
return false
}

View File

@@ -14,8 +14,8 @@ import java.util.function.Consumer
@Suppress("unused")
class TCP_PC_Command_Server {
private var tcpserver: ServerSocket? = null
private var job: Job? = null
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
/**
@@ -31,11 +31,11 @@ class TCP_PC_Command_Server {
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
while (isActive) {
if (tcpserver?.isClosed == true) break
if (tcpserver.isClosed) break
try {
tcpserver?.accept().use { socket ->
tcpserver.accept().use { socket ->
{
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
if (socket != null) {
val key : String = socket.inetAddress.hostAddress+":"+socket.port
socketMap[key] = socket
@@ -81,20 +81,18 @@ class TCP_PC_Command_Server {
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver?.close()
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job?.join()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
} finally {
tcpserver = null
}
return false
}

View File

@@ -13,5 +13,9 @@ enum class Category(name: String) {
Year("Year"),
Birthday("Birthday"),
Reason("Reason"),
Procedure("Procedure");
Sequence("Sequence"),
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")
enum class Language(name: String) {
INDONESIA("Indonesia"),
ENGLISH("English"),
LOCAL("Local"),
JAPANESE("Japanese"),
CHINESE("Chinese"),
ARABIC("Arabic");
INDONESIA("INDONESIA"),
ENGLISH("ENGLISH"),
LOCAL("LOCAL"),
JAPANESE("JAPANESE"),
CHINESE("CHINESE"),
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
@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
@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

@@ -20,4 +20,8 @@ data class Log(
return Log(0u, date, time, machine, description)
}
}
override fun toString() : String {
return "$datenya $timenya [$machine] $description"
}
}

View File

@@ -1,4 +1,9 @@
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

@@ -2,9 +2,11 @@ package database
import codes.Somecodes.Companion.ValiDateForLogHtml
import codes.Somecodes.Companion.toJsonString
import content.Category
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import max_channel
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
@@ -102,7 +104,7 @@ class MariaDB(
List.clear()
try {
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) {
val soundbank = Soundbank(
resultSet.getLong("index").toUInt(),
@@ -218,13 +220,15 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("INSERT INTO ${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM temp_${super.dbName}")
statement?.executeUpdate("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by Description" as Any)
// reload the local list
Get()
@@ -418,12 +422,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("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("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by Description" as Any)
// reload the local list
Get()
@@ -613,12 +619,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (TAG, Language) SELECT TAG, Language FROM ${super.dbName} ORDER BY TAG")
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("INSERT INTO ${super.dbName} (TAG, Language) SELECT TAG, Language FROM temp_${super.dbName}")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (TAG, Language) SELECT TAG, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by TAG" as Any)
// reload the local list
Get()
@@ -831,12 +839,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("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("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by Day and Time" as Any)
// reload the local list
Get()
@@ -985,7 +995,7 @@ class MariaDB(
override fun Add(data: BroadcastZones): Boolean {
try {
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(2, data.SoundChannel)
statement?.setString(3, data.id)
@@ -1007,7 +1017,7 @@ class MariaDB(
try {
connection.autoCommit = false
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)
for (bz in data) {
statement.setString(1, bz.description)
@@ -1030,7 +1040,7 @@ class MariaDB(
override fun UpdateByIndex(index: Int, data: BroadcastZones): Boolean {
try {
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(2, data.SoundChannel)
statement?.setString(3, data.id)
@@ -1052,12 +1062,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("INSERT INTO ${super.dbName} (description, SoundChannel, Box, Relay) SELECT description, SoundChannel, Box, Relay FROM temp_${super.dbName}")
statement?.executeUpdate("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by description" as Any)
// reload the local list
Get()
@@ -1073,7 +1085,7 @@ class MariaDB(
val sheet = workbook.getSheet("BroadcastZones")
?: throw Exception("No sheet named 'BroadcastZones' 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()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
@@ -1086,9 +1098,9 @@ class MariaDB(
val row = sheet.getRow(rowIndex) ?: continue
val description = row.getCell(1)?.stringCellValue ?: continue
val soundChannel = row.getCell(2)?.stringCellValue ?: continue
val box = row.getCell(3)?.stringCellValue ?: continue
val relay = row.getCell(4)?.stringCellValue ?: continue
val broadcastZone = BroadcastZones(0u, description, soundChannel, box, relay)
val id = row.getCell(3)?.stringCellValue ?: continue
val bp = row.getCell(4)?.stringCellValue ?: continue
val broadcastZone = BroadcastZones(0u, description, soundChannel, id, bp)
_broadcastZonesList.add(broadcastZone)
}
return AddAll(_broadcastZonesList)
@@ -1105,7 +1117,7 @@ class MariaDB(
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("BroadcastZones")
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()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
@@ -1116,8 +1128,8 @@ class MariaDB(
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("description"))
row.createCell(2).setCellValue(resultSet.getString("SoundChannel"))
row.createCell(3).setCellValue(resultSet.getString("Box"))
row.createCell(4).setCellValue(resultSet.getString("Relay"))
row.createCell(3).setCellValue(resultSet.getString("id"))
row.createCell(4).setCellValue(resultSet.getString("bp"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
@@ -1142,7 +1154,7 @@ class MariaDB(
"SB_TAGS VARCHAR(1024)," + // Comma-separated soundbank tags
"BroadcastZones VARCHAR(1024) NOT NULL," + // Comma-separated broadcast zones
"`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)
}
@@ -1176,7 +1188,7 @@ class MariaDB(
override fun Add(data: QueueTable): Boolean {
try {
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(2, data.Source)
@@ -1188,7 +1200,7 @@ class MariaDB(
statement?.setString(8, data.Language)
val rowsAffected = statement?.executeUpdate()
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
} else {
Logger.warn("No QueueTable entry added for: ${data.Message}" as Any)
@@ -1203,7 +1215,7 @@ class MariaDB(
try {
connection.autoCommit = false
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)
for (qt in data) {
statement.setString(1, qt.Date_Time)
@@ -1234,12 +1246,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("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("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
@@ -1308,7 +1322,7 @@ class MariaDB(
"Date_Time VARCHAR(45) NOT NULL," + // Date and time of the entry
"Source VARCHAR(45) NOT NULL," + // Source 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
")"
super.Create(tabledefinition)
@@ -1327,7 +1341,7 @@ class MariaDB(
resultSet.getString("Source"),
resultSet.getString("Type"),
resultSet.getString("Message"),
resultSet.getString("SB_TAGS"),
resultSet.getString("BroadcastZones"),
)
queueList.add(queuePaging)
List.add(queuePaging)
@@ -1393,12 +1407,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("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("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
@@ -1420,7 +1436,7 @@ class MariaDB(
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("QueuePaging")
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()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
@@ -1433,7 +1449,7 @@ class MariaDB(
row.createCell(2).setCellValue(resultSet.getString("Source"))
row.createCell(3).setCellValue(resultSet.getString("Type"))
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) {
sheet.autoSizeColumn(i)
@@ -1463,7 +1479,7 @@ class MariaDB(
val countResult = statement?.executeQuery("SELECT COUNT(*) AS count FROM ${super.dbName}")
if (countResult?.next() == true) {
val count = countResult.getInt("count")
if (count == 0) {
if (count < max_channel) {
Logger.info("SoundChannel table is empty, populating with default channels" as Any)
Clear()
}
@@ -1554,12 +1570,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} LIKE ${super.dbName}")
statement?.executeUpdate("INSERT INTO temp_${super.dbName} (channel, ip) SELECT channel, ip FROM ${super.dbName} ORDER BY `index` ")
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("INSERT INTO ${super.dbName} (channel, ip) SELECT channel, ip FROM temp_${super.dbName}")
statement?.executeUpdate("DROP TABLE temp_${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (channel, ip) SELECT channel, ip FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
@@ -1577,9 +1595,9 @@ class MariaDB(
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
Logger.info("${super.dbName} table cleared" as Any)
List.clear()
// create new rows from 1 to 64 with description "Channel 01" to "Channel 64" and empty ip
for (i in 1..64) {
val channel = String.format("Channel %02d", i)
// create new rows from 1 to 64 with description "Channel 1" to "Channel 64" and empty ip
for (i in 1..max_channel) {
val channel = String.format("Channel %d", i)
val insertStatement =
connection.prepareStatement("INSERT INTO ${super.dbName} (channel, ip) VALUES (?, ?)")
insertStatement?.setString(1, channel)
@@ -1727,13 +1745,12 @@ class MariaDB(
statement?.setString(4, data.description)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Log added: [$data.datenya $data.timenya] [$data.machine] $data.description" as Any)
return true
} else {
Logger.warn("No log entry added for: [$data.datenya $data.timenya] [$data.machine] $data.description" as Any)
Logger.warn{"Failed to add log entry : $data"}
}
} catch (e: Exception) {
Logger.error("Error adding log entry: ${e.message}" as Any)
Logger.error{"Error adding log entry: ${e.message}"}
}
return false
}
@@ -1768,19 +1785,21 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// 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(
"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} " +
"ORDER BY datenya , timenya , machine "
)
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate(
"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)
// reload the local list
Get()
@@ -1837,7 +1856,8 @@ class MariaDB(
"username VARCHAR(100) NOT NULL," +
"password 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
"broadcastzones TEXT NOT NULL"+ // Comma-separated broadcast zones
")"
@@ -1855,7 +1875,8 @@ class MariaDB(
resultSet.getString("username"),
resultSet.getString("password"),
resultSet.getString("location"),
resultSet.getString("soundbank_tags"),
resultSet.getString("airline_tags"),
resultSet.getString("city_tags"),
resultSet.getString("messagebank_ann_id"),
resultSet.getString("broadcastzones")
)
@@ -1869,13 +1890,14 @@ class MariaDB(
override fun Add(data: UserDB): Boolean {
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(2, data.password)
statement?.setString(3, data.location)
statement?.setString(4, data.soundbank_tags)
statement?.setString(5, data.messagebank_ann_id)
statement?.setString(6, data.broadcastzones)
statement?.setString(4, data.airline_tags)
statement?.setString(5, data.city_tags)
statement?.setString(6, data.messagebank_ann_id)
statement?.setString(7, data.broadcastzones)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("User added: ${data.username}" as Any)
@@ -1892,15 +1914,16 @@ class MariaDB(
override fun AddAll(data: ArrayList<UserDB>): Boolean {
return try {
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)
for (user in data) {
statement.setString(1, user.username)
statement.setString(2, user.password)
statement.setString(3, user.location)
statement.setString(4, user.soundbank_tags)
statement.setString(5, user.messagebank_ann_id)
statement.setString(6, user.broadcastzones)
statement.setString(4, user.airline_tags)
statement.setString(5, user.city_tags)
statement.setString(6, user.messagebank_ann_id)
statement.setString(7, user.broadcastzones)
statement.addBatch()
}
statement.executeBatch()
@@ -1916,14 +1939,15 @@ class MariaDB(
override fun UpdateByIndex(index: Int, data: UserDB): Boolean {
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(2, data.password)
statement?.setString(3, data.location)
statement?.setString(4, data.soundbank_tags)
statement?.setString(5, data.messagebank_ann_id)
statement?.setString(6, data.broadcastzones)
statement?.setLong(7, index.toLong())
statement?.setString(4, data.airline_tags)
statement?.setString(5, data.city_tags)
statement?.setString(6, data.messagebank_ann_id)
statement?.setString(7, data.broadcastzones)
statement?.setLong(8, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("User updated at index $index: ${data.username}" as Any)
@@ -1940,12 +1964,14 @@ class MariaDB(
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_${super.dbName} 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("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
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("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("DROP TABLE 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 $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
@@ -1960,7 +1986,7 @@ class MariaDB(
try {
val sheet = workbook.getSheet("User") ?: throw Exception("No sheet named 'User' 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()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
@@ -1974,10 +2000,11 @@ class MariaDB(
val username = row.getCell(1)?.stringCellValue ?: continue
val password = row.getCell(2)?.stringCellValue ?: continue
val location = row.getCell(3)?.stringCellValue ?: continue
val soundbank_tags = row.getCell(4)?.stringCellValue ?: continue
val messagebank_ann_id = row.getCell(5)?.stringCellValue ?: continue
val broadcastzones = row.getCell(6)?.stringCellValue ?: continue
val user = UserDB(0u, username, password, location, soundbank_tags, messagebank_ann_id, broadcastzones)
val airline_tags = row.getCell(4)?.stringCellValue ?: continue
val city_tags = row.getCell(5)?.stringCellValue ?: continue
val messagebank_ann_id = row.getCell(6)?.stringCellValue ?: continue
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)
}
return AddAll(_userList)
@@ -1994,7 +2021,7 @@ class MariaDB(
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("User")
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()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
@@ -2006,9 +2033,10 @@ class MariaDB(
row.createCell(1).setCellValue(resultSet.getString("username"))
row.createCell(2).setCellValue(resultSet.getString("password"))
row.createCell(3).setCellValue(resultSet.getString("location"))
row.createCell(4).setCellValue(resultSet.getString("soundbank_tags"))
row.createCell(5).setCellValue(resultSet.getString("messagebank_ann_id"))
row.createCell(6).setCellValue(resultSet.getString("broadcastzones"))
row.createCell(4).setCellValue(resultSet.getString("airline_tags"))
row.createCell(5).setCellValue(resultSet.getString("city_tags"))
row.createCell(6).setCellValue(resultSet.getString("messagebank_ann_id"))
row.createCell(7).setCellValue(resultSet.getString("broadcastzones"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
@@ -2239,6 +2267,120 @@ class MariaDB(
return null
}
/**
* Find all city soundbank by tag
* @param tag The tags to search for
* @return a list of Soundbank with Category City and matching tag
*/
fun Find_Soundbank_City(tag: String) : List<Soundbank>{
return soundDB.List
.filter{ it.Category== Category.City.name }
.filter { it.TAG.equals(tag,true)}
}
fun Find_Soundbank_AirplaneName(tag: String) : List<Soundbank>{
return soundDB.List
.filter{ it.Category== Category.Airplane_Name.name }
.filter { it.TAG.equals(tag,true)}
}
fun Find_Soundbank_Places(tag: String) : List<Soundbank>{
return soundDB.List
.filter{ it.Category== Category.Places.name }
.filter { it.TAG.equals(tag,true)}
}
fun Find_Soundbank_Shalat(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.Shalat.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_Sequence(tag: String) : List<Soundbank>{
return soundDB.List
.filter{ it.Category== Category.Sequence.name }
.filter { it.TAG.equals(tag,true)}
}
fun Find_Soundbank_Reason(tag: String) : List<Soundbank>{
return soundDB.List
.filter{ it.Category== Category.Reason.name }
.filter { it.TAG.equals(tag,true)}
}
fun Find_Soundbank_Procedure(tag: String) : List<Soundbank> {
return soundDB.List
.filter { it.Category == Category.Procedure.name }
.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 Message_Detail: 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
@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,4 +1,11 @@
package database
@Suppress("unused")
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 {
return "QueuePaging(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', BroadcastZones='$BroadcastZones')"
}
}

View File

@@ -1,4 +1,14 @@
package database
@Suppress("unused")
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 {
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 Enable: Boolean,
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
@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
data class Soundbank(
var index: UInt,
var Description: String,
@@ -8,4 +9,32 @@ data class Soundbank(
var Language: String,
var VoiceType: 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
@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 codes.Somecodes
import codes.Somecodes.Companion.GetSensorsInfo
import codes.Somecodes.Companion.GetUptime
import codes.Somecodes.Companion.ListAudioFiles
import codes.Somecodes.Companion.ValiDateForLogHtml
import codes.Somecodes.Companion.ValidFile
@@ -9,6 +11,7 @@ import codes.Somecodes.Companion.ValidIPV4
import codes.Somecodes.Companion.ValidScheduleDay
import codes.Somecodes.Companion.ValidScheduleTime
import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.ValidStrings
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import content.Category
@@ -16,12 +19,13 @@ import content.Language
import content.ScheduleDay
import content.VoiceType
import database.BroadcastZones
import database.BroadcastZonesHtml
import database.LanguageLink
import database.MariaDB
import database.Messagebank
import database.ScheduleBank
import database.SoundChannel
import database.Soundbank
import database.UserDB
import db
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.before
@@ -35,6 +39,7 @@ import io.javalin.http.Context
import io.javalin.json.JavalinJackson
import io.javalin.websocket.WsMessageContext
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.nio.file.Files
import java.time.LocalDateTime
@Suppress("unused")
@@ -53,6 +58,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
fun Start() {
app = Javalin.create { config ->
config.useVirtualThreads = true
config.staticFiles.add("/webpage")
@@ -83,7 +89,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
// Set user session
it.sessionAttribute("user", user.first)
println("User ${user.first} logged in")
//println("User ${user.first} logged in")
// Redirect to home page
it.redirect("home.html")
}
@@ -93,28 +99,32 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
before { CheckUsers(it) }
ws("/ws") { ws ->
// WebSocket endpoint for home
ws.onClose { wsCloseContext ->
// TODO Handle WebSocket close event
println("WebSocket closed: ${wsCloseContext.session.remoteAddress}")
}
//
ws.onMessage { wsMessageContext ->
try {
val cmd =
objectmapper.readValue(wsMessageContext.message(), WebsocketCommand::class.java)
when (cmd.command) {
"getSystemTime" -> {
val systemtime = LocalDateTime.now().format(Somecodes.datetimeformat1)
val uptime = GetUptime()
SendReply(
wsMessageContext,
cmd.command,
LocalDateTime.now().format(Somecodes.datetimeformat1)
if (uptime.isNotEmpty()) "Date & Time : $systemtime\nSystem Uptime : $uptime" else "Date & Time : $systemtime"
)
}
"getCPUStatus" -> {
Somecodes.getCPUUsage { vv ->
val sv = GetSensorsInfo()
if (sv.isNotEmpty()){
SendReply(wsMessageContext, cmd.command, vv+"\n"+sv)
} else {
SendReply(wsMessageContext, cmd.command, vv)
}
}
}
"getMemoryStatus" -> {
SendReply(wsMessageContext, cmd.command, Somecodes.getMemoryUsage())
@@ -125,8 +135,9 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
"getNetworkStatus" -> {
// TODO Get Network status
SendReply(wsMessageContext, cmd.command, "OK")
Somecodes.GetNetworkStatus { nn ->
SendReply(wsMessageContext, cmd.command, objectmapper.writeValueAsString(nn))
}
}
"getPagingQueue" ->{
@@ -138,7 +149,8 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
"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 -> {
@@ -150,10 +162,9 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
}
ws.onConnect { wsConnectContext ->
// TODO Handle WebSocket connect event
println("WebSocket connected: ${wsConnectContext.session.remoteAddress}")
}
// ws.onConnect { wsConnectContext ->
// println("WebSocket connected: ${wsConnectContext.session.remoteAddress}")
// }
}
}
@@ -196,43 +207,99 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
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") {
get("List") {
it.result(MariaDB.ArrayListtoString(db.soundDB.List))
}
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") {
try {
val addvalue = objectmapper.readValue(it.body(), Soundbank::class.java)
if (ValidString(addvalue.Description)) {
if (ValidString(addvalue.TAG)) {
if (ValidString(addvalue.Category)) {
if (ValidString(addvalue.Language)) {
if (ValidString(addvalue.Path)) {
// check apakah TAG sudah ada untuk language dan category yang sama
if (addvalue.isNotEmpty()){
// check apakah TAG sudah ada untuk language, category dan voicetype yang sama
val exists = db.soundDB.List.any { sb ->
sb.TAG == addvalue.TAG && sb.Language == addvalue.Language && sb.Category == addvalue.Category
sb.TAG == addvalue.TAG && sb.Language == addvalue.Language && sb.Category == addvalue.Category && sb.VoiceType == addvalue.VoiceType
}
if (!exists) {
if (ValidFile(addvalue.Path)) {
val absolutepath = Somecodes.SoundbankDirectory(Language.valueOf(addvalue.Language), VoiceType.valueOf(addvalue.VoiceType), Category.valueOf(addvalue.Category)).resolve(addvalue.Path)
if (Files.isRegularFile(absolutepath)) {
addvalue.Path = absolutepath.toAbsolutePath().toString()
if (db.soundDB.Add(addvalue)) {
db.soundDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Added Sound Bank: $addvalue")
} else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to add soundbank to database")))
} else it.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Invalid Path, file does not exist")))
} else it.status(400)
.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("Invalid Path")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Language")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Category")))
} 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")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Some fields are empty")))
} catch (e: Exception) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid request body, Message: ${e.message}")))
}
}
delete("List") {
@@ -240,6 +307,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.soundDB.Clear()) {
db.soundDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Sound Bank table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate soundbank table")))
}
@@ -253,6 +321,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.soundDB.DeleteByIndex(index.toInt())) {
db.soundDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Sound Bank with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete soundbank with index $index")))
}
@@ -261,68 +330,79 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
patch("UpdateByIndex/{index}") {
// update by index
val index = it.pathParam("index").toUIntOrNull()
if (index == null) {
// tidak ada path param index
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid index")))
} else {
if (index!=null){
val sb = db.soundDB.List.find { xx -> xx.index == index }
if (sb == null) {
// soundbank dengan index tersebut tidak ditemukan
it.status(404).result(objectmapper.writeValueAsString(resultMessage("Soundbank with index $index not found")))
} else {
// soundbank dengan index tersebut ditemukan, sekarang update
val json: JsonNode = objectmapper.readTree(it.body())
if (json.isEmpty) {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("UpdateByIndex with index=$index has empty body")))
} else {
val _description = json.get("Description").asText("")
val _tag = json.get("TAG").asText("")
val _category = json.get("Category").asText("")
val _language = json.get("Language").asText("")
val _path = json.get("Path").asText("")
var changed = false
if (ValidString(_description) && _description != sb.Description) {
sb.Description = _description
changed = true
if (sb!=null){
try{
//println("index=$index, body=${it.body()}")
val newsb = objectmapper.readValue(it.body(), Soundbank::class.java)
//println("newsb=$newsb")
if (newsb.isNotEmpty()){
var varchanged = false
if (newsb.Description != sb.Description) {
sb.Description = newsb.Description
varchanged = true
}
if (ValidString(_tag) && _tag != sb.TAG) {
sb.TAG = _tag
changed = true
if (newsb.TAG != sb.TAG) {
sb.TAG = newsb.TAG
varchanged = true
}
if (newsb.ValidCategory()) {
if (newsb.Category != sb.Category){
sb.Category = newsb.Category
varchanged = true
}
if (ValidString(_category) && _category != sb.Category) {
if (Category.entries.any { cat ->
cat.name == _category
}) {
sb.Category = _category
changed = true
} else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Category")))
return@patch
}
if (newsb.ValidLanguage()) {
if (newsb.Language != sb.Language){
sb.Language = newsb.Language
varchanged = true
}
if (ValidString(_language) && _language != sb.Language) {
sb.Language = _language
changed = true
}
if (ValidString(_path) && _path != sb.Path) {
if (ValidFile(_path)) {
sb.Path = _path
changed = true
} 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
}
if (newsb.ValidVoiceType()) {
if (newsb.VoiceType != sb.VoiceType){
sb.VoiceType = newsb.VoiceType
varchanged = true
}
if (changed) {
} else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid VoiceType")))
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") {
val xlsxdata = db.soundDB.Export_XLSX()
@@ -363,6 +443,13 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
// get messagebank list
it.result(MariaDB.ArrayListtoString(db.messageDB.List))
}
get("MessageIDs") { ctx ->
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("") ?: ""
@@ -381,6 +468,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.Add(mb)){
db.messageDB.Resort()
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(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Message_TAGS")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Message_Detail")))
@@ -394,6 +482,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.Clear()) {
db.messageDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Message Bank table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate messagebank table")))
}
@@ -407,6 +496,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.DeleteByIndex(index.toInt())) {
db.messageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Message Bank with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete messagebank with index $index")))
}
@@ -467,6 +557,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.messageDB.UpdateByIndex(index.toInt(), mb)) {
db.messageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Messagebank $mb")
} else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update messagebank with index $index")))
} else it.status(400)
@@ -519,7 +610,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
val json: JsonNode = objectmapper.readTree(it.body())
val tag = json.get("tag").asText("")
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 (languages.all { xx -> Language.entries.any { yy -> yy.name.equals(xx,true)} }){
if (!db.languageDB.List.any { ll -> ll.TAG.equals(tag,true) }) {
@@ -527,13 +618,14 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.Add(newvalue)){
db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Added Language Link: $newvalue")
} else {
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 {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("TAG=$tag already exists")))
println("TAG=$tag already exists")
//println("TAG=$tag already exists")
}
} else {
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Contains unsupported language")))
@@ -549,6 +641,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.Clear()) {
db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Language Link table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate language link table")))
}
@@ -562,6 +655,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.DeleteByIndex(index.toInt())) {
db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Language Link with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete language link with index $index")))
}
@@ -596,6 +690,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.languageDB.UpdateByIndex(index.toInt(), ll)) {
db.languageDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Language Link $ll")
} else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update language link with index $index")))
} else it.status(400)
@@ -648,10 +743,57 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.scheduleDB.Clear()) {
db.scheduleDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Schedule Bank table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate schedulebank table")))
}
}
post("Add"){
// 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 by index
val index = it.pathParam("index").toUIntOrNull()
@@ -661,6 +803,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.scheduleDB.DeleteByIndex(index.toInt())) {
db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Schedule Bank with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete schedule with index $index")))
}
@@ -740,6 +883,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.scheduleDB.UpdateByIndex(index.toInt(), sb)) {
db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Schedule : $sb")
} else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update schedule with index $index")))
} else it.status(400)
@@ -775,12 +919,204 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
db.scheduleDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
} 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) {
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") {
get("List") { get1 ->
@@ -830,24 +1166,20 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
path("BroadcastZones"){
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(newlist))
it.result(MariaDB.ArrayListtoString(db.broadcastDB.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
if (db.broadcastDB.Clear()) {
db.broadcastDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Broadcast Zones table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate broadcast zones table")))
}
@@ -866,6 +1198,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.broadcastDB.Add(newbp)){
db.broadcastDB.Resort()
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(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Relay")))
} else it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid Box")))
@@ -881,6 +1214,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.broadcastDB.DeleteByIndex(index.toInt())) {
db.broadcastDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Deleted Broadcast Zone with index $index")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to delete broadcast zone with index $index")))
}
@@ -925,6 +1259,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
if (db.broadcastDB.UpdateByIndex(index.toInt(), bz)) {
db.broadcastDB.Resort()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Updated Broadcast Zone : $bz")
} else it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update broadcast zone with index $index")))
} else it.status(400)
@@ -973,11 +1308,15 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
get("List"){
it.result(MariaDB.ArrayListtoString(db.soundchannelDB.List))
}
get("SoundChannelDescriptions") {
it.result(objectmapper.writeValueAsString(db.Get_SoundChannel_List()))
}
delete("List") {
// truncate sound channel table
if (db.soundchannelDB.Clear()) {
db.soundchannelDB.Get()
it.result(objectmapper.writeValueAsString(resultMessage("OK")))
db.Add_Log("AAS", "Clear Sound Channel table")
} else {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate sound channel table")))
}
@@ -1011,25 +1350,28 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
} else {
// 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) }) {
println("Another sound channel already uses IP $_ip and channel $_channel")
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Another sound channel already uses IP $_ip and channel $_channel")))
return@patch
}
val othersc = db.soundchannelDB.List.filter { sc -> sc.ip == _ip }.filter { sc -> sc.index != index }
if (othersc.isNotEmpty()){
it.status(400).result(objectmapper.writeValueAsString(resultMessage("This IP address is already used by another sound channel")))
} 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")))
return@patch
it.status(500)
.result(objectmapper.writeValueAsString(resultMessage("Failed to update sound channel with index $index")))
}
}
}
} else {
println("Invalid IP address")
it.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid IP address")))
@@ -1044,7 +1386,6 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
}
}
get("ExportXLSX"){
val xlsxdata = db.soundchannelDB.Export_XLSX()
@@ -1076,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")))
}
} 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)