Compare commits

...

92 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
rdkartono
54775641bb Commit 30/09/2025 2025-09-30 16:03:28 +07:00
rdkartono
85776cce45 Commit 30/09/2025 2025-09-30 14:44:31 +07:00
rdkartono
cf24c06b35 Commit 29/09/2025 2025-09-29 11:56:08 +07:00
rdkartono
f18a0ca9cd Commit 25/09/2025 2025-09-25 16:02:55 +07:00
rdkartono
21592c1405 Commit 25/09/2025 2025-09-25 15:54:33 +07:00
rdkartono
e18a08ab6a Commit 25/09/2025 2025-09-25 11:28:52 +07:00
rdkartono
7db10bd45a Commit 25/09/2025 2025-09-25 08:18:19 +07:00
09de205afc commit 24/09/2025 2025-09-25 07:29:57 +07:00
1fcf64fd99 commit 24/09/2025 2025-09-24 16:03:07 +07:00
55f6a24cce commit 24/09/2025 2025-09-24 11:48:36 +07:00
59dd67acc8 commit 23/09/2025 2025-09-23 16:13:32 +07:00
85ccf05634 commit 23/09/2025 2025-09-23 13:56:16 +07:00
7c67fe90ee commit 18/09/2025 2025-09-22 15:18:39 +07:00
120c8e5276 commit 18/09/2025 2025-09-18 15:57:11 +07:00
09d074aa03 commit 12/09/2025 2025-09-12 16:31:25 +07:00
b692e2c2c9 commit 11/09/2025 2025-09-11 16:03:33 +07:00
34fc71cfbc commit 10/09/2025 2025-09-10 16:08:24 +07:00
f48ead1b44 commit 10/09/2025 2025-09-10 12:05:56 +07:00
40f462ce79 commit 09/09/2025 2025-09-09 15:54:42 +07:00
c01c4e39fd commit 03/09/2025 2025-09-03 16:01:18 +07:00
ff4f0fd742 commit 03/09/2025 2025-09-03 13:51:38 +07:00
ea04f8d316 commit 03/09/2025 2025-09-03 11:05:30 +07:00
ea1defa78e commit 02/09/2025 2025-09-02 12:34:15 +07:00
7100cf826d commit 28/08/2025 2025-08-28 12:08:16 +07:00
ef9c17a65c commit 28/08/2025 2025-08-28 10:49:56 +07:00
c73b181ef5 commit 26/08/2025 2025-08-27 07:16:51 +07:00
09b65738af commit 26/08/2025 2025-08-26 16:13:37 +07:00
4743b47a89 commit 26/08/2025 2025-08-26 08:24:31 +07:00
0c84449b77 commit 20/08/2025 2025-08-20 16:42:06 +07:00
4f0a7a1560 commit 28/07/2025 2025-07-28 16:33:41 +07:00
d12a089eda commit 28/07/2025 2025-07-28 16:03:04 +07:00
b743146a2c commit 28/07/2025 2025-07-28 16:00:51 +07:00
49e82aae0e commit 28/07/2025 2025-07-28 15:49:53 +07:00
901d553da9 commit 28/07/2025 2025-07-28 13:55:58 +07:00
9b1851aaa7 commit 26/07/2025 2025-07-26 10:36:15 +07:00
3d94fe3520 commit 26/07/2025 2025-07-26 10:32:39 +07:00
a32a1d3300 first commit 26/07/2025 2025-07-26 10:14:46 +07:00
f6ae8e14c0 first commit 26/07/2025 2025-07-26 08:11:05 +07:00
123 changed files with 22387 additions and 186 deletions

8
.gitignore vendored
View File

@@ -29,4 +29,10 @@ bin/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
## Soundbank directories ##
PagingResult/
SoundBank/
SoundbankResult/
logs/

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

9
.idea/dataSources.xml generated
View File

@@ -1,15 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="mariadB" uuid="a738dd17-8123-478b-81aa-6ecf4f890ccc">
<driver-ref>mariadb</driver-ref>
<data-source source="LOCAL" name="mysql" uuid="6f68a2ce-92f6-4203-a8c0-f18965b0d627">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mariadb://localhost:3306/aas</jdbc-url>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/aas</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@@ -1,8 +1,14 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ClassName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ConstPropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="EnumEntryName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="FunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="LocalVariableName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MethodNameSameAsClassName" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantCast" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />

20
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="0dccb4d0-b5b2-4677-8ad3-caea8077052f" />
<option name="name" value="0dccb4d0-b5b2-4677-8ad3-caea8077052f" />
<option name="url" value="https://mvnrepository.com/" />
</remote-repository>
</component>
</project>

View File

@@ -0,0 +1,12 @@
<component name="libraryTable">
<library name="fasterxml.jackson.core.databind" type="repository">
<properties maven-id="com.fasterxml.jackson.core:jackson-databind:2.17.2" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-databind/2.17.2/jackson-databind-2.17.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-annotations/2.17.2/jackson-annotations-2.17.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-core/2.17.2/jackson-core-2.17.2.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -0,0 +1,17 @@
<component name="libraryTable">
<library name="fasterxml.jackson.module.kotlin" type="repository">
<properties maven-id="com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/module/jackson-module-kotlin/2.17.2/jackson-module-kotlin-2.17.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-databind/2.17.2/jackson-databind-2.17.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-core/2.17.2/jackson-core-2.17.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-annotations/2.17.2/jackson-annotations-2.17.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.7.22/kotlin-reflect-1.7.22.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.7.22/kotlin-stdlib-1.7.22.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.22/kotlin-stdlib-common-1.7.22.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

13
.idea/libraries/github_oshi_core.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="github.oshi.core" type="repository">
<properties maven-id="com.github.oshi:oshi-core:6.9.0" />
<CLASSES>
<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 />
</library>
</component>

11
.idea/libraries/google_code_gson.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="google.code.gson" type="repository">
<properties maven-id="com.google.code.gson:gson:2.13.1" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.38.0/error_prone_annotations-2.38.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -1,37 +1,28 @@
<component name="libraryTable">
<library name="io.javalin" type="repository">
<properties maven-id="io.javalin:javalin:5.4.2" />
<properties maven-id="io.javalin:javalin:LATEST" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/javalin/javalin/5.4.2/javalin-5.4.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.6/slf4j-api-2.0.6.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-server/11.0.14/jetty-server-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/javalin/javalin/6.7.0/javalin-6.7.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-server/11.0.25/jetty-server-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-http/11.0.25/jetty-http-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-util/11.0.25/jetty-util-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-io/11.0.25/jetty-io-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/toolchain/jetty-jakarta-servlet-api/5.0.2/jetty-jakarta-servlet-api-5.0.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-http/11.0.14/jetty-http-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-util/11.0.14/jetty-util-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-io/11.0.14/jetty-io-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-webapp/11.0.14/jetty-webapp-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-servlet/11.0.14/jetty-servlet-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-security/11.0.14/jetty-security-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-xml/11.0.14/jetty-xml-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-server/11.0.14/websocket-jetty-server-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-common/11.0.14/websocket-jetty-common-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-core-common/11.0.14/websocket-core-common-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-servlet/11.0.14/websocket-servlet-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-core-server/11.0.14/websocket-core-server-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-annotations/11.0.14/jetty-annotations-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-plus/11.0.14/jetty-plus-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/jakarta/transaction/jakarta.transaction-api/2.0.0/jakarta.transaction-api-2.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-jndi/11.0.14/jetty-jndi-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/ow2/asm/asm/9.4/asm-9.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/ow2/asm/asm-commons/9.4/asm-commons-9.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/ow2/asm/asm-tree/9.4/asm-tree-9.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-api/11.0.14/websocket-jetty-api-11.0.14.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.7.10/kotlin-stdlib-jdk8-1.7.10.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.7.10/kotlin-stdlib-1.7.10.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.7.10/kotlin-stdlib-common-1.7.10.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-server/11.0.25/websocket-jetty-server-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-servlet/11.0.25/jetty-servlet-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-security/11.0.25/jetty-security-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-webapp/11.0.25/jetty-webapp-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-xml/11.0.25/jetty-xml-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-api/11.0.25/websocket-jetty-api-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-common/11.0.25/websocket-jetty-common-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-core-common/11.0.25/websocket-core-common-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-servlet/11.0.25/websocket-servlet-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-core-server/11.0.25/websocket-core-server-11.0.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.9.25/kotlin-stdlib-jdk8-1.9.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.25/kotlin-stdlib-1.9.25.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.7.10/kotlin-stdlib-jdk7-1.7.10.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.9.25/kotlin-stdlib-jdk7-1.9.25.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />

View File

@@ -0,0 +1,22 @@
<component name="libraryTable">
<library name="kotlinx-coroutines-core" type="repository">
<properties maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0/kotlinx-coroutines-core-1.9.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/23.0.0/annotations-23.0.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.0/kotlin-stdlib-2.0.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/23.0.0/annotations-23.0.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.0/kotlin-stdlib-2.0.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.9.0/kotlinx-coroutines-core-1.9.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.9.0/kotlinx-coroutines-core-jvm-1.9.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/23.0.0/annotations-23.0.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.0/kotlin-stdlib-2.0.0-sources.jar!/" />
</SOURCES>
</library>
</component>

11
.idea/libraries/mysql_connector_j.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="mysql.connector.j" type="repository">
<properties maven-id="com.mysql:mysql-connector-j:8.4.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/mysql/mysql-connector-j/8.4.0/mysql-connector-j-8.4.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.25.1/protobuf-java-3.25.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

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 />

11
.idea/libraries/slf4j_simple.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="slf4j.simple" type="repository">
<properties maven-id="org.slf4j:slf4j-simple:2.0.17" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-simple/2.0.17/slf4j-simple-2.0.17.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

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 />

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">

3
.idea/sqldialects.xml generated
View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/database/MariaDB.kt" dialect="GenericSQL" />
<file url="PROJECT" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/database/MariaDB.kt" dialect="MySQL" />
</component>
</project>

View File

@@ -8,6 +8,8 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<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" />
@@ -15,6 +17,24 @@
<orderEntry type="library" name="tinylog.impl" level="project" />
<orderEntry type="library" name="net.java.dev.jna" level="project" />
<orderEntry type="library" name="io.javalin" level="project" />
<orderEntry type="library" name="mariadb.jdbc.java.client" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="library" name="slf4j.simple" level="project" />
<orderEntry type="library" name="fasterxml.jackson.module.kotlin" level="project" />
<orderEntry type="library" name="fasterxml.jackson.core.databind" level="project" />
<orderEntry type="library" name="github.oshi.core" level="project" />
<orderEntry type="library" name="mysql.connector.j" level="project" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="file://C:/SLC/Apache POI" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://C:/SLC/Apache POI" />
</SOURCES>
<jarDirectory url="file://C:/SLC/Apache POI" recursive="false" />
<jarDirectory url="file://C:/SLC/Apache POI" recursive="false" type="SOURCES" />
</library>
</orderEntry>
</component>
</module>

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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,79 @@
.bs-icon {
--bs-icon-size: .75rem;
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
font-size: var(--bs-icon-size);
width: calc(var(--bs-icon-size) * 2);
height: calc(var(--bs-icon-size) * 2);
color: var(--bs-primary);
}
.bs-icon-xs {
--bs-icon-size: 1rem;
width: calc(var(--bs-icon-size) * 1.5);
height: calc(var(--bs-icon-size) * 1.5);
}
.bs-icon-sm {
--bs-icon-size: 1rem;
}
.bs-icon-md {
--bs-icon-size: 1.5rem;
}
.bs-icon-lg {
--bs-icon-size: 2rem;
}
.bs-icon-xl {
--bs-icon-size: 2.5rem;
}
.bs-icon.bs-icon-primary {
color: var(--bs-white);
background: var(--bs-primary);
}
.bs-icon.bs-icon-primary-light {
color: var(--bs-primary);
background: rgba(var(--bs-primary-rgb), .2);
}
.bs-icon.bs-icon-semi-white {
color: var(--bs-primary);
background: rgba(255, 255, 255, .5);
}
.bs-icon.bs-icon-rounded {
border-radius: .5rem;
}
.bs-icon.bs-icon-circle {
border-radius: 50%;
}
.pad-icon {
display: flex;
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

@@ -0,0 +1,320 @@
body {
background-color: #f8f9fd;
/*background-color: #edf1fb;*/
overflow-x: hidden;
width: 100%;
}
.pad-header {
padding: 1em;
}
.pad-button {
margin-bottom: 0.5em;
}
.search {
/*display: flex;*/
/*align-items: center;*/
margin-top: -0.2rem;
}
.text-header {
color: #2d3578;
}
.bg-header {
background-color: #f0f2ff !important;
}
.text-right {
text-align: right;
}
.bg-status-1 {
background-color: #ffffff;
}
.bg-status-2 {
background-color: #dce5f4;
}
.neu-button {
background-color: #f5f5f5;
border-radius: 20px;
box-shadow: inset 4px 4px 10px #88a5bf7b, inset -4px -4px 10px #ffffff;
/*color: #4d4d4d;*/
cursor: pointer;
font-size: 16px;
/*padding: 15px 40px;*/
transition: all 0.2s ease-in-out;
border: 1px solid #88a5bf7b;
}
.neu-button:hover {
box-shadow: inset 2px 2px 5px #bcbcbc, inset -2px -2px 5px #ffffff, 2px 2px 5px #bcbcbc, -2px -2px 5px #ffffff;
}
.neu-button:focus {
outline: none;
box-shadow: inset 2px 2px 5px #bcbcbc, inset -2px -2px 5px #ffffff, 2px 2px 5px #bcbcbc, -2px -2px 5px #ffffff;
}
.btn-round-basic:focus {
background-color: #f5f5f5;
border-radius: 8px;
box-shadow: inset 4px 4px 10px #88a5bf7b, inset -4px -4px 10px #ffffff;
/*color: #4d4d4d;*/
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease-in-out;
border: 1px solid #88a5bf7b;
}
.btn-round-basic {
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;
}
.btn-round-basic:hover {
outline: none;
box-shadow: inset 2px 2px 5px #bcbcbc, inset -2px -2px 5px #ffffff, 2px 2px 5px #bcbcbc, -2px -2px 5px #ffffff;
}
.color-import, .color-import:hover, .color-import:focus {
color: var(--bs-teal);
}
.color-remove, .color-remove:hover, .color-remove:focus {
color: var(--bs-danger);
}
.color-edit, .color-edit:hover, .color-edit:focus {
color: var(--bs-primary-text-emphasis);
}
.color-add, .color-add:hover, .color-add:focus {
color: var(--bs-primary);
}
.input-login {
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: 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;
}
.input-add {
border-radius: 8px;
padding: .375rem .75rem;
margin: 0.3rem 0;
}
.text-add {
margin: 0.6rem 0;
}
.display-show {
display: block;
}
.class25 {
width: 25%;
}
.card-login {
background-color: white;
box-shadow: rgba(0, 0, 0, 0.15) 0px 15px 25px, rgba(0, 0, 0, 0.05) 0px 5px 10px;
border-radius: 20px;
border: none;
}
.icon-menu {
color: #2c316d;
}
.text-menu {
color: #2c316d !important;
}
nav-item:hover {
background-color: #2d3578;
color: white;
}
nav-item:focus {
background-color: #4450b1;
color: white;
}
.img-indicator {
display: block;
margin-left: auto;
max-width: 100%;
}
.font-top-menu {
font-size: 2em!important;
font-weight: 200;
padding-left: 0.5em;
}
.card-status {
background-color: var(--card-color);
box-shadow: rgba(0, 0, 0, 0.1) -4px 9px 25px -6px;
text-align: center;
width: 100%;
padding: 15px;
margin: 10px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 0rem;
}
.text-status {
margin-bottom: 0rem !important;
}
.pad-container {
padding-top: 1em;
}
.bread-menu {
background: #2d3578;
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,332 @@
/**
* @typedef {Object} BroadcastZone
* @property {number} index
* @property {string} description
* @property {String} SoundChannel
* @property {String} Box
* @property {String} Relay
*/
/**
* List of broadcast zones available
* @type {BroadcastZone[]}
*/
window.BroadcastZoneList ??= [];
/**
* Currently selected broadcast zone row in the table
* @type {JQuery<HTMLElement>|null}
*/
window.selectedBroadcastZoneRow = null;
/**
* List of sound channels available
* @type {String[]}
*/
window.SoundChannelList = []
/**
* Fill broadcast zone table body with values
* @param {BroadcastZone[]} vv values to fill
*/
function fill_broadcastzonetablebody(vv) {
$('#broadcastzonetablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.description}</td>
<td>${item.soundChannel}</td>
<td>${item.id}</td>
<td>${item.bp}</td>
</tr>`;
$('#broadcastzonetablebody').append(row);
let $addedrow = $('#broadcastzonetablebody tr:last');
$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');
window.selectedBroadcastZoneRow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload broadcast zones from server
* @param {String} APIURL API URL endpoint (default "BroadcastZones/")
*/
function reloadBroadcastZones(APIURL = "BroadcastZones/") {
window.BroadcastZoneList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadBroadcastZones : ", okdata)
window.BroadcastZoneList.push(...okdata);
fill_broadcastzonetablebody(window.BroadcastZoneList);
} else console.log("reloadBroadcastZones: okdata is not array");
}, (errdata) => {
alert("Error loading broadcast zones : " + errdata.message);
});
}
function fetchSoundChannels(APIURL = "SoundChannel/") {
window.SoundChannelList = [];
fetchAPI(APIURL + "SoundChannelDescriptions", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("fetchSoundChannels : ", okdata)
window.SoundChannelList.push(...okdata);
} else console.log("fetchSoundChannels: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
$(document).ready(function () {
console.log("broadcastzones.js loaded successfully");
window.selectedBroadcastZoneRow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnEdit = $('#btnEdit');
let $btnRemove = $('#btnRemove');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
$btnEdit.prop('disabled', true);
$btnRemove.prop('disabled', true);
let APIURL_BroadcastZone = "BroadcastZones/";
let $broadcastzonemodal = $('#broadcastzonemodal');
let $broadcastzoneindex = $broadcastzonemodal.find('#broadcastzoneindex');
let $broadcastzonedescription = $broadcastzonemodal.find('#broadcastzonedescription');
let $broadcastzonesoundchannel = $broadcastzonemodal.find('#broadcastzonesoundchannel');
let $broadcastzonebox = $broadcastzonemodal.find('#broadcastzonebox');
let $findzone = $('#findzone');
$findzone.off('input').on('input', function () {
let searchTerm = $findzone.val().trim().toLowerCase();
if (searchTerm.length > 0) {
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 {
window.selectedBroadcastZoneRow = null;
fill_broadcastzonetablebody(window.BroadcastZoneList);
}
});
/**
* Find Checkbox for relays 1 to 32
* Checkbox id is 01 to 32 with leading zero for 1 to 9
* @param {number} id 1 - 32
* @returns JQuery<HTMLElement>
*/
function cbRelay(id) {
return $broadcastzonemodal.find('#R' + (id < 10 ? '0' : '') + id);
}
/**
* Clear broadcast zone modal to default state
*/
function clearBroadcastZoneModal() {
$broadcastzoneindex.prop('disabled', true).val('');
$broadcastzonedescription.val('');
// fill broadcastzonesoundchannel from SoundChannelList
$broadcastzonesoundchannel.empty();
console.log("SoundChannelList:", window.SoundChannelList);
if (Array.isArray(window.SoundChannelList) && window.SoundChannelList.length > 0) {
// SoundChannelList ada isinya
window.SoundChannelList.forEach(ch => {
if (ch && ch.length>0){
$broadcastzonesoundchannel.append($('<option>').val(ch).text(ch));
}
});
}
$broadcastzonebox.val('1').prop('disabled', true);
for (let i = 1; i <= 32; i++) {
cbRelay(i).prop('checked', false);
}
}
fetchSoundChannels();
reloadBroadcastZones(APIURL_BroadcastZone);
$btnClear.off('click').on('click', () => {
DoClear(APIURL_BroadcastZone, "BroadcastZones", (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success clear broadcast zones: " + okdata.message);
}, (errdata) => {
alert("Error clear broadcast zones: " + errdata.message);
});
});
$btnAdd.off('click').on('click', () => {
$broadcastzonemodal.modal('show');
clearBroadcastZoneModal();
$broadcastzonemodal.off('click.broadcastzonesave').on('click.broadcastzonesave', '#broadcastzonesave', function () {
let description = $broadcastzonedescription.val().trim();
let soundChannel = $broadcastzonesoundchannel.val().trim();
let box = $broadcastzonebox.val().trim();
let relayArray = [];
for (let i = 1; i <= 32; i++) {
if (cbRelay(i).is(':checked')) {
relayArray.push(i);
}
}
let relay = relayArray.join(';');
if (description.length === 0) {
alert("Description cannot be empty");
return;
}
if (soundChannel.length === 0) {
alert("Sound Channel cannot be empty");
return;
}
if (box.length === 0) {
alert("Box cannot be empty");
return;
}
if (relayArray.length === 0) {
alert("At least one relay must be selected");
return;
}
let bz = {
description: description,
SoundChannel: soundChannel,
Box: box,
Relay: relay
};
fetchAPI(APIURL_BroadcastZone + "Add", "POST", {}, bz, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success add new broadcast zone: " + okdata.message);
}, (errdata) => {
alert("Error add new broadcast zone: " + errdata.message);
});
$broadcastzonemodal.modal('hide');
});
$broadcastzonemodal.off('click.broadcastzoneclose').on('click.broadcastzoneclose', '#broadcastzoneclose', function () {
$broadcastzonemodal.modal('hide');
});
});
$btnRemove.off('click').on('click', () => {
if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */
let bz = {
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 delete broadcast zone [${bz.index}] Description=${bz.description}?`)) {
fetchAPI(APIURL_BroadcastZone + "DeleteByIndex/" + bz.index, "DELETE", {}, null, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success delete broadcast zone: " + okdata.message);
}, (errdata) => {
alert("Error delete broadcast zone: " + errdata.message);
});
}
}
});
$btnEdit.off('click').on('click', () => {
if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */
let bz = {
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.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.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();
let soundChannel = $broadcastzonesoundchannel.val().trim();
let box = $broadcastzonebox.val().trim();
let relayArray = [];
for (let i = 1; i <= 32; i++) {
if (cbRelay(i).is(':checked')) {
relayArray.push(i);
}
}
let relay = relayArray.join(';');
if (description.length === 0) {
alert("Description cannot be empty");
return;
}
if (soundChannel.length === 0) {
alert("Sound Channel cannot be empty");
return;
}
if (box.length === 0) {
alert("Box cannot be empty");
return;
}
if (relayArray.length === 0) {
alert("At least one relay must be selected");
return;
}
let bzUpdate = {
description: description,
SoundChannel: soundChannel,
Box: box,
Relay: relay
};
fetchAPI(APIURL_BroadcastZone + "UpdateByIndex/" + bz.index, "PATCH", {}, bzUpdate, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success edit broadcast zone: " + okdata.message);
}, (errdata) => {
alert("Error edit broadcast zone: " + errdata.message);
});
$broadcastzonemodal.modal('hide');
});
$broadcastzonemodal.off('click.broadcastzoneclose').on('click.broadcastzoneclose', '#broadcastzoneclose', function () {
$broadcastzonemodal.modal('hide');
});
}
}
});
$btnExport.off('click').on('click', () => {
DoExport(APIURL_BroadcastZone, "broadcastzones.xlsx", {});
});
$btnImport.off('click').on('click', () => {
DoImport(APIURL_BroadcastZone, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success import broadcast zones: " + okdata.message);
}, (errdata) => {
alert("Error importing broadcast zones from XLSX: " + errdata.message);
});
});
});

View File

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

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

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,272 @@
/**
* @typedef {Object} LanguageBank
* @property {number} index
* @property {string} tag
* @property {string} language
*
*/
/** List of Languagebank data loaded from server
* @type {LanguageBank[]}
*/
window.languagebankdata = [];
/**
* Currently selected languagebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
window.selectedlanguagerow = null;
/**
* Fill languagebank table body with values
* @param {LanguageBank[]} vv values to fill
*/
function fill_languagebanktablebody(vv) {
$('#languagebanktablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.tag}</td>
<td>${item.language}</td>
</tr>`;
$('#languagebanktablebody').append(row);
let $addedrow = $('#languagebanktablebody tr:last');
$addedrow.click(function () {
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');
window.selectedlanguagerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload language bank from server
* @param {string} APIURL API URL endpoint, default "LanguageLink/"
*/
function reloadLanguageBank(APIURL = "LanguageLink/") {
window.languagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.languagebankdata.push(...okdata);
window.selectedlanguagerow = null;
fill_languagebanktablebody(window.languagebankdata);
}
}, (errdata) => {
alert("Error loading languagebank : " + errdata.message);
});
}
$(document).ready(function () {
console.log('languagebank.js loaded');
$('#languagebanktablebody').empty();
window.selectedlanguagerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
$btnRemove.prop('disabled', true);
$btnEdit.prop('disabled', true);
let APIURL = "LanguageLink/";
let $findlanguage = $('#findlanguage');
let $modal = $('#languagemodal');
let $langid = $modal.find('#languagelinkindex');
let $langtag = $modal.find('#languagelinktag');
let $cbInd = $modal.find('#langId');
let $cbLocal = $modal.find('#langLocal');
let $cbEn = $modal.find('#langEn');
let $cbArb = $modal.find('#langArb');
let $cbJap = $modal.find('#langJap');
let $cbChi = $modal.find('#langChi');
function clearLanguageModal() {
$langid.prop('disabled', true).val('');
$langtag.val('');
$cbInd.prop('checked', false);
$cbLocal.prop('checked', false);
$cbEn.prop('checked', false);
$cbArb.prop('checked', false);
$cbJap.prop('checked', false);
$cbChi.prop('checked', false);
}
$findlanguage.on('input', function () {
let searchTerm = $findlanguage.val().toLowerCase();
if (searchTerm.length > 0) {
window.selectedlanguagerow = null;
let filtered = window.languagebankdata.filter(item => item.tag.toLowerCase().includes(searchTerm) || item.language.toLowerCase().includes(searchTerm));
fill_languagebanktablebody(filtered);
} else {
window.selectedlanguagerow = null;
fill_languagebanktablebody(window.languagebankdata);
}
});
reloadLanguageBank(APIURL);
$btnClear.click(() => {
DoClear(APIURL, "LanguageLink", (okdata) => {
reloadLanguageBank(APIURL);
alert("Success clear languageLink : " + okdata.message);
}, (errdata) => {
alert("Error clear languageLink : " + errdata.message);
});
});
$btnAdd.click(() => {
// show modal with id 'languagemodal'
$modal.modal('show');
clearLanguageModal();
// save button click event
$modal.off('click.languagelinksave').on('click.languagelinksave', '#languagelinksave', function () {
const tag = $langtag.val();
const langs = [];
if ($cbInd.is(':checked')) langs.push('INDONESIA');
if ($cbLocal.is(':checked')) langs.push('LOCAL');
if ($cbEn.is(':checked')) langs.push('ENGLISH');
if ($cbArb.is(':checked')) langs.push('ARABIC');
if ($cbJap.is(':checked')) langs.push('JAPANESE');
if ($cbChi.is(':checked')) langs.push('CHINESE');
if (tag.length === 0) {
alert("Tag cannot be empty");
return;
}
if (langs.length === 0) {
alert("At least one language must be selected");
return;
}
const langString = langs.join(';');
let ll = {
tag: tag,
language: langString
}
fetchAPI(APIURL + "Add", "POST", {}, ll, (okdata) => {
alert("Success add language : " + okdata.message);
reloadLanguageBank(APIURL);
}, (errdata) => {
alert("Error add language : " + errdata.message);
});
$modal.modal('hide');
});
// close button click event
$modal.off('click.languagelinkclose').on('click.languagelinkclose', '#languagelinkclose', function () {
$modal.modal('hide');
});
});
$btnRemove.click(() => {
if (window.selectedlanguagerow) {
let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */
let ll = {
index: Number(cells.eq(0).text()),
tag: cells.eq(1).text(),
language: cells.eq(2).text()
}
if (confirm(`Are you sure to delete language [${ll.index}] Tag=${ll.tag} Language=${ll.language}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + ll.index, "DELETE", {}, null, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success delete language : " + okdata.message);
}, (errdata) => {
alert("Error delete language : " + errdata.message);
});
}
}
});
$btnEdit.click(() => {
if (window.selectedlanguagerow) {
let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */
let ll = {
index: Number(cells.eq(0).text()),
tag: cells.eq(1).text(),
language: cells.eq(2).text()
}
if (confirm(`Are you sure to edit language [${ll.index}] Tag=${ll.tag} Language=${ll.language}?`)) {
clearLanguageModal();
$langid.val(ll.index);
$langtag.val(ll.tag);
let langs = ll.language.toUpperCase().split(';');
$cbInd.prop('checked', langs.includes('INDONESIA'));
$cbLocal.prop('checked', langs.includes('LOCAL'));
$cbEn.prop('checked', langs.includes('ENGLISH'));
$cbArb.prop('checked', langs.includes('ARABIC'));
$cbJap.prop('checked', langs.includes('JAPANESE'));
$cbChi.prop('checked', langs.includes('CHINESE'));
$modal.modal('show');
// save button click event
$modal.off('click.languagelinksave').on('click.languagelinksave', '#languagelinksave', function () {
const tag = $langtag.val();
const langs = [];
if ($cbInd.is(':checked')) langs.push('INDONESIA');
if ($cbLocal.is(':checked')) langs.push('LOCAL');
if ($cbEn.is(':checked')) langs.push('ENGLISH');
if ($cbArb.is(':checked')) langs.push('ARABIC');
if ($cbJap.is(':checked')) langs.push('JAPANESE');
if ($cbChi.is(':checked')) langs.push('CHINESE');
if (tag.length === 0) {
alert("Tag cannot be empty");
return;
}
if (langs.length === 0) {
alert("At least one language must be selected");
return;
}
const langString = langs.join(';');
if (ll.tag === tag && ll.language === langString) {
alert("No changes detected");
$modal.modal('hide');
return;
}
ll.tag = tag;
ll.language = langString;
fetchAPI(APIURL + "UpdateByIndex/" + ll.index, "PATCH", {}, ll, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success edit language : " + okdata.message);
}, (errdata) => {
alert("Error edit language : " + errdata.message);
});
$modal.modal('hide');
});
// close button click event
$modal.off('click.languagelinkclose').on('click.languagelinkclose', '#languagelinkclose', function () {
$modal.modal('hide');
});
}
}
});
$btnExport.click(() => {
DoExport(APIURL, "languagebank.xlsx", {});
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success import languagebank : " + okdata.message);
}, (errdata) => {
alert("Error importing languagebank from XLSX : " + errdata.message);
});
});
});

View File

@@ -0,0 +1,95 @@
/**
* @typedef {Object} Log
* @property {number} index
* @property {string} datenya
* @property {string} timenya
* @property {string} machine
* @property {string} description
*/
/** List of Log data loaded from server
* @type {Log[]}
*/
window.logdata = [];
/**
* Fill log table body with values
* @param {Log[]} vv values to fill
*/
function fill_logtablebody(vv) {
$('#logtablebody').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.datenya}</td>
<td>${item.timenya}</td>
<td>${item.machine}</td>
<td>${item.description}</td>
</tr>`;
$('#logtablebody').append(row);
});
$('#tablesize').text("Table Size: " + vv.length);
$('#btnExport').prop('disabled', false);
}
/**
* Reload logs from server with date and filter
* @param {String} APIURL API URL endpoint , default "Log/"
* @param {String} date date in format dd-mm-yyyy
* @param {String} filter log filter text
*/
function reloadLogs(APIURL = "Log/", date, filter) {
const params = new URLSearchParams({
date: date,
filter: filter
})
window.logdata = [];
fetchAPI(APIURL + "List?" + params.toString(), "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.logdata.push(...okdata);
fill_logtablebody(window.logdata);
}
}, (errdata) => {
alert("Error loading logs : " + errdata.message);
});
}
$(document).ready(function () {
console.log("log.js ready");
let selectedlogdate = "";
let logfilter = "";
let APIURL = "Log/";
$('#logtablebody').empty();
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}`);
selectedlogdate = `${dd}-${mm}-${yyyy}`;
reloadLogs(APIURL, selectedlogdate, logfilter);
}
$('#logdate').off('change').on('change', function () {
const selected = $(this).val();
if (selected) {
const [year, month, day] = selected.split('-');
selectedlogdate = `${day}-${month}-${year}`;
reloadLogs(APIURL, selectedlogdate, logfilter);
}
});
$('#searchfilter').off('input').on('input', function () {
logfilter = $(this).val();
reloadLogs(APIURL, selectedlogdate, logfilter);
});
$('#btnExport').off('click').on('click', function () {
DoExport(APIURL, "log.xlsx", { date: selectedlogdate, filter: logfilter });
});
});

View File

@@ -0,0 +1,456 @@
/**
* @typedef {Object} MessageBank
* @property {number} index
* @property {string} description
* @property {string} language
* @property {number} aNN_ID
* @property {string} voice_Type
* @property {string} message_Detail
* @property {string} message_TAGS
*/
/**
* List of Messagebank data loaded from server
* @type {MessageBank[]}
*/
window.messagebankdata ??= [];
/**
* Currently selected messagebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
window.selectedmessagerow = null;
/**
* Fill messagebank table body with values
* @param {MessageBank[]} vv values to fill
*/
function fill_messagebanktablebody(vv) {
$('#messagebanktablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.description}</td>
<td>${item.language}</td>
<td>${item.aNN_ID}</td>
<td>${item.voice_Type}</td>
<td>${item.message_Detail}</td>
<td>${item.message_TAGS}</td>
</tr>`;
$('#messagebanktablebody').append(row);
let $addedrow = $('#messagebanktablebody tr:last');
$addedrow.click(function () {
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');
window.selectedmessagerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload message bank from server
* @param {string} APIURL API URL endpoint, default "MessageBank/"
*/
function reloadMessageBank(APIURL = "MessageBank/") {
window.messagebankdata ??= [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.messagebankdata.push(...okdata);
window.selectedmessagerow = null;
fill_messagebanktablebody(window.messagebankdata);
}
}, (errdata) => {
alert("Error loading messagebank : " + errdata.message);
});
}
$(document).ready(function () {
console.log("messagebank.js loaded");
$('#messagebanktablebody').empty();
window.selectedmessagerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
$btnRemove.prop('disabled', true);
$btnEdit.prop('disabled', true);
let APIURL = "MessageBank/";
let $findmessage = $('#findmessage');
// modal for add / edit messagebank
let $modal = $('#messagebankmodal');
// text input, disabled by default
let $messageindex = $modal.find('#messageindex');
// text input
let $messagedescription = $modal.find('#messagedescription');
// select input, options loaded from languages[]
let $messagelanguage = $modal.find('#messagelanguage');
// number input from 1 to 100
let $messageannid = $modal.find('#messageannid');
// select input, options loaded from voiceTypes[]
let $messagevoicetype = $modal.find('#messagevoicetype');
// list <ul> of available categories and phrases
let $messageavailablevariables = $modal.find('#messageavailablevariables');
// list <ul> of selected categories and phrases
let $messageselectedvariables = $modal.find('#messageselectedvariables');
// for clearing messageselectedvariables
let $btnclearlist = $modal.find('#btnclearlist');
// for removing selected item from messageselectedvariables
let $btnremovefromlist = $modal.find('#btnremovefromlist');
// for adding selected item from messageavailablevariables to messageselectedvariables
let $btnaddtolist = $modal.find('#btnaddtolist');
/**
* Refill messageavailablevariables options from categories[]
* and soundbankdata with category "Phrase" if messagelanguage and messagevoicetype are selected
*/
function refill_messageavailablevariables() {
$messageavailablevariables.empty();
categories.forEach(cat => {
$messageavailablevariables.append(ListItem(`[${cat}]`));
});
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);
});
}
}
}
/**
* Clear message modal to default state
*/
function clearMessageModal() {
$messageindex.val('').prop('disabled', true);
$messagedescription.val('');
// fill messagelanguage options from languages[]
$messagelanguage.empty();
window.languages.forEach(lang => {
$messagelanguage.append(new Option(lang, lang));
});
$messagelanguage.val(null);
$messagelanguage.on('change', function () {
refill_messageavailablevariables();
});
// set default annid to 1
$messageannid.val(1);
// fill messagevoicetype options from voiceTypes[]
$messagevoicetype.empty();
window.voiceTypes.forEach(vt => {
$messagevoicetype.append(new Option(vt, vt));
});
$messagevoicetype.val(null);
$messagevoicetype.on('change', function () {
refill_messageavailablevariables();
});
refill_messageavailablevariables();
$messageselectedvariables.empty();
// event on btnclearlist
$btnclearlist.off('click').on('click', function () {
if ($messageselectedvariables.children().length > 0) {
if (confirm("Are you sure want to clear selected variables list?")) {
$messageselectedvariables.empty();
}
}
});
// event on btnremovefromlist
$btnremovefromlist.off('click').on('click', function () {
let $selected = $messageselectedvariables.find('option:selected');
if ($selected.length > 0) {
$selected.remove();
}
});
// event on btnaddtolist
$btnaddtolist.off('click').on('click', function () {
let $selected = $messageavailablevariables.find('option:selected');
if ($selected.length > 0) {
$selected.each(function () {
$messageselectedvariables.append($(this).clone());
});
}
});
}
$findmessage.on('input', function () {
let searchTerm = $findmessage.val().toLowerCase();
if (searchTerm.length > 0) {
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 {
window.selectedmessagerow = null;
fill_messagebanktablebody(window.messagebankdata);
}
});
reloadMessageBank(APIURL);
$btnClear.click(() => {
DoClear(APIURL, "Messagebank", (okdata) => {
reloadMessageBank(APIURL);
alert("Success clear messagebank : " + okdata.message);
}, (errdata) => {
alert("Error clear messagebank : " + errdata.message);
});
});
$btnAdd.click(() => {
$modal.modal('show');
clearMessageModal();
// event on Click save button
$modal.off('click.messagebanksave').on('click.messagebanksave', '#messagebanksave', function () {
let description = $messagedescription.val().trim();
let language = $messagelanguage.val();
let annid = parseInt($messageannid.val());
let voicetype = $messagevoicetype.val();
let messagedetail = "";
let messagetags = "";
// iterate messageselectedvariables children
$messageselectedvariables.children().each(function () {
let val = $(this).text().trim();
if (val.length > 0) {
if (val.startsWith('[') && val.endsWith(']')) {
// categories
messagetags += (messagetags.length > 0 ? " " : "") + val;
messagedetail += (messagedetail.length > 0 ? " " : "") + val;
} else {
// phrases
// find in soundbankdata by description with specified language and voicetype
let sb = soundbankdata
.filter(sb => sb.language.toLowerCase() === language.toLowerCase())
.filter(sb => sb.voiceType.toLowerCase() === voicetype.toLowerCase())
.find(sb => sb.Description.toLowerCase() === val.toLowerCase());
if (sb) {
messagedetail += (messagedetail.length > 0 ? " " : "") + sb.Description;
messagetags += (messagetags.length > 0 ? " " : "") + sb.tag;
}
}
}
});
if (description.length === 0) {
alert("Description cannot be empty");
return;
}
if (!language) {
alert("Language cannot be empty");
return;
}
if (isNaN(annid) || annid < 1 || annid > 100) {
alert("ANN_ID must be a number between 1 and 100");
return;
}
if (!voicetype) {
alert("Voice Type cannot be empty");
return;
}
if (messagedetail.length === 0 || messagetags.length === 0) {
alert("Message haven't been constructed, please add categories and phrases");
return;
}
let mb = {
Description: description,
Language: language,
ANN_ID: annid,
Voice_Type: voicetype,
Message_Detail: messagedetail,
Message_TAGS: messagetags
};
// send to server using fetchAPI
fetchAPI(APIURL + "Add", "POST", mb, null, (okdata) => {
reloadMessageBank(APIURL);
alert("Success add new messagebank : " + okdata.message);
}, (errdata) => {
alert("Error add new messagebank : " + errdata.message);
});
$modal.modal('hide');
});
// event on Click close button
$modal.off('click.messagebankclose').on('click.messagebankclose', '#messagebankclose', function () {
$modal.modal('hide');
});
});
$btnRemove.click(() => {
if (window.selectedmessagerow) {
let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */
let mb = {
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
language: cells.eq(2).text(),
aNN_ID: parseInt(cells.eq(3).text()),
voice_Type: cells.eq(4).text(),
message_Detail: cells.eq(5).text(),
message_TAGS: cells.eq(6).text()
}
if (confirm(`Are you sure to delete messagebank [${mb.index}] Description=${mb.description}? ANN_ID=${mb.aNN_ID} Language=${mb.language} Voice_Type=${mb.voice_Type} `)) {
fetchAPI(APIURL + "DeleteByIndex/" + mb.index, "DELETE", {}, null, (okdata) => {
reloadMessageBank(APIURL);
alert("Success delete messagebank : " + okdata.message);
}, (errdata) => {
alert("Error delete messagebank : " + errdata.message);
});
}
}
});
$btnEdit.click(() => {
if (window.selectedmessagerow) {
let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */
let mb = {
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
language: cells.eq(2).text(),
aNN_ID: parseInt(cells.eq(3).text()),
voice_Type: cells.eq(4).text(),
message_Detail: cells.eq(5).text(),
message_TAGS: cells.eq(6).text()
}
if (confirm(`Are you sure to edit messagebank [${mb.index}] Description=${mb.description} ANN_ID=${mb.aNN_ID} Language=${mb.language} Voice_Type=${mb.voice_Type} `)) {
$modal.modal('show');
clearMessageModal();
// Fill modal fields with selected messagebank data
$messageindex.val(mb.index).prop('disabled', true);
$messagedescription.val(mb.description);
$messagelanguage.val(mb.language);
$messagevoicetype.val(mb.voice_Type);
$messageannid.val(mb.aNN_ID);
// Refill message available variables
refill_messageavailablevariables();
// Fill messageselectedvariables from message_Detail and message_TAGS
$messageselectedvariables.empty();
if (mb.message_Detail) {
mb.message_Detail.split(' ').forEach(val => {
$messageselectedvariables.append(ListItem(val));
});
}
// Save button event
$modal.off('click.messagebanksave').on('click.messagebanksave', '#messagebanksave', function () {
let description = $messagedescription.val().trim();
let language = $messagelanguage.val();
let annid = parseInt($messageannid.val());
let voicetype = $messagevoicetype.val();
let messagedetail = "";
let messagetags = "";
$messageselectedvariables.children().each(function () {
let val = $(this).text().trim();
if (val.length > 0) {
if (val.startsWith('[') && val.endsWith(']')) {
messagetags += (messagetags.length > 0 ? " " : "") + val;
messagedetail += (messagedetail.length > 0 ? " " : "") + val;
} else {
let sb = soundbankdata
.filter(sb => sb.language.toLowerCase() === language.toLowerCase())
.filter(sb => sb.voiceType.toLowerCase() === voicetype.toLowerCase())
.find(sb => sb.Description && sb.Description.toLowerCase() === val.toLowerCase());
if (sb) {
messagedetail += (messagedetail.length > 0 ? " " : "") + sb.Description;
messagetags += (messagetags.length > 0 ? " " : "") + sb.tag;
}
}
}
});
if (description.length === 0) {
alert("Description cannot be empty");
return;
}
if (!language) {
alert("Language cannot be empty");
return;
}
if (isNaN(annid) || annid < 1 || annid > 100) {
alert("ANN_ID must be a number between 1 and 100");
return;
}
if (!voicetype) {
alert("Voice Type cannot be empty");
return;
}
if (messagedetail.length === 0 || messagetags.length === 0) {
alert("Message haven't been constructed, please add categories and phrases");
return;
}
let mbUpdate = {
Description: description,
Language: language,
ANN_ID: annid,
Voice_Type: voicetype,
Message_Detail: messagedetail,
Message_TAGS: messagetags
};
fetchAPI(APIURL + "UpdateByIndex/" + mb.index, "PATCH", mbUpdate, null, (okdata) => {
reloadMessageBank(APIURL);
alert("Success edit messagebank : " + okdata.message);
}, (errdata) => {
alert("Error edit messagebank : " + errdata.message);
});
$modal.modal('hide');
});
// Close button event
$modal.off('click.messagebankclose').on('click.messagebankclose', '#messagebankclose', function () {
$modal.modal('hide');
});
}
}
});
$btnExport.click(() => {
DoExport(APIURL, "messagebank.xlsx", {});
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadMessageBank(APIURL);
alert("Success import messagebank : " + okdata.message);
}, (errdata) => {
alert("Error importing messagebank from XLSX : " + errdata.message);
});
});
});

View File

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

View File

@@ -0,0 +1,400 @@
/**
* @typedef {Object} ScheduleBank
* @property {number} index
* @property {string} description
* @property {string} day
* @property {string} time
* @property {string} soundpath
* @property {number} repeat
* @property {boolean} enable
* @property {string} broadcastZones
* @property {string} language
*/
/** List of Schedulebank data loaded from server
* @type {ScheduleBank[]}
*/
window.schedulebankdata = [];
/**
* Currently selected schedulebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
window.selectedschedulerow = null;
/**
* Fill schedulebank table body with values
* @param {ScheduleBank[]} vv values to fill
*/
function fill_schedulebanktablebody(vv) {
$('#schedulebanktablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.description}</td>
<td>${item.day}</td>
<td>${item.time}</td>
<td>${item.soundpath}</td>
<td>${item.repeat}</td>
<td>${item.enable}</td>
<td>${item.broadcastZones}</td>
<td>${item.language}</td>
</tr>`;
$('#schedulebanktablebody').append(row);
let $addedrow = $('#schedulebanktablebody tr:last');
$addedrow.click(function () {
if (selectedschedulerow) {
selectedschedulerow.find('td').css('background-color', '');
if (selectedschedulerow.is($(this))) {
selectedschedulerow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$addedrow.find('td').css('background-color', '#ffeeba');
selectedschedulerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload timer bank from server
* @param {string} APIURL API URL endpoint, default "ScheduleBank/"
*/
function reloadTimerBank(APIURL = "ScheduleBank/") {
window.schedulebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.schedulebankdata.push(...okdata);
selectedschedulerow = null;
fill_schedulebanktablebody(window.schedulebankdata);
}
}, (errdata) => {
alert("Error loading schedulebank : " + errdata.message);
});
}
$(document).ready(function () {
console.log("schedulebank.js loaded successfully");
$('#schedulebanktablebody').empty();
selectedschedulerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnEdit = $('#btnEdit');
let $btnRemove = $('#btnRemove');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
$btnEdit.prop('disabled', true);
$btnRemove.prop('disabled', true);
let APIURL = "ScheduleBank/";
let $schedulemodal = $('#schedulemodal');
// text input
let $scheduleid = $schedulemodal.find('#scheduleid');
// text input
let $scheduledescription = $schedulemodal.find('#scheduledescription');
// number input 0-23
let $schedulehour = $schedulemodal.find('#schedulehour');
// number input 0-59
let $scheduleminute = $schedulemodal.find('#scheduleminute');
// 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');
// select2 for broadcastzones
let $schedulezones = $schedulemodal.find('#schedulezones');
$schedulezones.select2({});
// radio button for everyday
let $scheduleeveryday = $schedulemodal.find('#scheduleeveryday');
// 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')) {
$scheduledate.prop('disabled', false);
} else {
$scheduledate.prop('disabled', true);
}
});
function clearScheduleModal() {
$scheduleid.prop('disabled', true).val('');
$scheduledescription.val('');
$schedulehour.val('0');
$scheduleminute.val('0');
$schedulerepeat.val('0');
$scheduleenable.prop('checked', true);
$scheduleeveryday.prop('checked', false);
$schedulespecialdate.prop('checked', false);
$scheduledate.prop('disabled', true).val('');
}
let $findschedule = $('#findschedule');
$findschedule.on('input', function () {
let searchTerm = $findschedule.val().toLowerCase();
if (searchTerm.length > 0) {
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 {
window.selectedschedulerow = null;
fill_schedulebanktablebody(window.schedulebankdata);
}
});
reloadTimerBank(APIURL);
$btnClear.click(() => {
DoClear(APIURL, "Timerbank", (okdata) => {
reloadTimerBank(APIURL);
alert("Success clear schedulebank : " + okdata.message);
}, (errdata) => {
alert("Error clear schedulebank : " + errdata.message);
});
});
$btnAdd.click(() => {
$schedulemodal.modal('show');
clearScheduleModal();
$schedulemodal.off('click.scheduleclose').on('click.scheduleclose', '#scheduleclose', function () {
$schedulemodal.modal('hide');
});
$schedulemodal.off('click.schedulesave').on('click.schedulesave', '#schedulesave', function () {
// Gather form values
const Description = $scheduledescription.val();
const Soundpath = $schedulesoundpath.val();
const Repeat = parseInt($schedulerepeat.val(), 10);
const Enable = $scheduleenable.is(':checked');
// Collect selected days
let Day = "";
if ($scheduleeveryday.is(':checked')) {
Day = "Everyday";
} else if ($schedulespecialdate.is(':checked')) {
Day = $scheduledate.val();
} else {
if ($schedulesunday.is(':checked')) Day = "Sunday";
if ($schedulemonday.is(':checked')) Day = "Monday";
if ($scheduletuesday.is(':checked')) Day = "Tuesday";
if ($schedulewednesday.is(':checked')) Day = "Wednesday";
if ($schedulethursday.is(':checked')) Day = "Thursday";
if ($schedulefriday.is(':checked')) Day = "Friday";
if ($schedulesaturday.is(':checked')) Day = "Saturday";
}
// Broadcast zones (assuming comma-separated string)
const BroadcastZones = $schedulezones.val();
// Validate required fields
if (!Description || !Soundpath || Day === "") {
alert("Description, sound path, and day are required.");
return;
}
// Format time as HH:mm
const hour = parseInt($schedulehour.val(), 10);
const minute = parseInt($scheduleminute.val(), 10);
const Time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
// Prepare object
const scheduleObj = {
Description,
Day,
Time,
Soundpath,
Repeat,
Enable,
BroadcastZones
};
fetchAPI(APIURL + "Add", "POST", {}, scheduleObj, (okdata) => {
alert("Success add schedule: " + okdata.message);
reloadTimerBank(APIURL);
}, (errdata) => {
alert("Error add schedule: " + errdata.message);
});
$schedulemodal.modal('hide');
});
});
$btnRemove.click(() => {
if (window.selectedschedulerow) {
let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */
let sr = {
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
day: cells.eq(2).text(),
time: cells.eq(3).text(),
soundpath: cells.eq(4).text(),
repeat: cells.eq(5).text(),
enable: cells.eq(6).text(),
broadcastZones: cells.eq(7).text(),
language: cells.eq(8).text()
}
if (confirm(`Are you sure to delete schedule [${sr.index}] Description=${sr.description}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sr.index, "DELETE", {}, null, (okdata) => {
reloadTimerBank(APIURL);
alert("Success delete schedule : " + okdata.message);
}, (errdata) => {
alert("Error delete schedule : " + errdata.message);
});
}
}
});
$btnEdit.click(() => {
if (window.selectedschedulerow) {
let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */
let sr = {
index: Number(cells.eq(0).text()),
description: cells.eq(1).text(),
day: cells.eq(2).text(),
time: cells.eq(3).text(),
soundpath: cells.eq(4).text(),
repeat: cells.eq(5).text(),
enable: cells.eq(6).text(),
broadcastZones: cells.eq(7).text(),
language: cells.eq(8).text()
}
if (confirm(`Are you sure to edit schedule [${sr.index}] Description=${sr.description}?`)) {
$schedulemodal.modal('show');
// fill the form with existing data
$scheduleid.val(sr.index);
$scheduledescription.val(sr.description);
let [hour, minute] = sr.time.split(':').map(num => parseInt(num, 10));
$schedulehour.val(hour.toString());
$scheduleminute.val(minute.toString());
$schedulesoundpath.val(sr.soundpath);
$schedulerepeat.val(sr.repeat.toString());
$scheduleenable.prop('checked', sr.enable.toLowerCase() === 'true');
switch (sr.day) {
case 'Everyday':
$scheduleeveryday.click();
break;
case 'Sunday':
$schedulesunday.click();
break;
case 'Monday':
$schedulemonday.click();
break;
case 'Tuesday':
$scheduletuesday.click();
break;
case 'Wednesday':
$schedulewednesday.click();
break;
case 'Thursday':
$schedulethursday.click();
break;
case 'Friday':
$schedulefriday.click();
break;
case 'Saturday':
$schedulesaturday.click();
break;
default:
// check if the day is in format dd/mm/yyyy
// and set the special date radio button and date input
if (/^\d{2}\/\d{2}\/\d{4}$/.test(sr.day)) {
$schedulespecialdate.click();
$scheduledate.val(sr.day);
}
}
$schedulemodal.off('click.scheduleclose').on('click.scheduleclose', '#scheduleclose', function () {
$schedulemodal.modal('hide');
});
$schedulemodal.off('click.schedulesave').on('click.schedulesave', '#schedulesave', function () {
// Gather form values
const Description = $scheduledescription.val();
const Soundpath = $schedulesoundpath.val();
const Repeat = parseInt($schedulerepeat.val(), 10);
const Enable = $scheduleenable.is(':checked');
// Collect selected days
let Day = "";
if ($scheduleeveryday.is(':checked')) {
Day = "Everyday";
} else if ($schedulespecialdate.is(':checked')) {
Day = $scheduledate.val();
} else {
if ($schedulesunday.is(':checked')) Day = "Sunday";
if ($schedulemonday.is(':checked')) Day = "Monday";
if ($scheduletuesday.is(':checked')) Day = "Tuesday";
if ($schedulewednesday.is(':checked')) Day = "Wednesday";
if ($schedulethursday.is(':checked')) Day = "Thursday";
if ($schedulefriday.is(':checked')) Day = "Friday";
if ($schedulesaturday.is(':checked')) Day = "Saturday";
}
// Broadcast zones (assuming comma-separated string)
const BroadcastZones = $schedulezones.val();
// Validate required fields
if (!Description || !Soundpath || Day === "") {
alert("Description, sound path, and day are required.");
return;
}
// Format time as HH:mm
const hour = parseInt($schedulehour.val(), 10);
const minute = parseInt($scheduleminute.val(), 10);
const Time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
// Prepare object
const scheduleObj = {
Description,
Day,
Time,
Soundpath,
Repeat,
Enable,
BroadcastZones
};
fetchAPI(APIURL + "UpdateByIndex/" + sr.index, "PATCH", {}, scheduleObj, (okdata) => {
alert("Success edit schedule: " + okdata.message);
reloadTimerBank(APIURL);
}, (errdata) => {
alert("Error edit schedule: " + errdata.message);
});
$schedulemodal.modal('hide');
});
}
}
});
$btnExport.click(() => {
DoExport(APIURL, "schedulebank.xlsx", {});
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadTimerBank(APIURL);
alert("Success import schedulebank from XLSX : " + okdata.message);
}, (errdata) => {
alert("Error importing schedulebank from XLSX : " + errdata.message);
});
});
});

View File

@@ -0,0 +1,512 @@
/**
* List of voice types available
* @type {string[]}
*/
window.voiceTypes = [];
/**
* List of categories available
* @type {string[]}
*/
window.categories = [];
/**
* List of languages available
* @type {string[]}
*/
window.languages = [];
/**
* List of scheduled days available
* @type {string[]}
*/
window.scheduledays = []
/**
* Create a list item element
* @param {String} text Text Content for the list item
* @param {String} className Specific class name for the list item
* @returns {JQuery<HTMLElement>}
*/
function ListItem(text, className = "") {
return $('<li>').addClass(className).text(text);
}
/**
* WebSocket connection
* @type {WebSocket}
*/
window.ws = null;
/**
* Send a command to the WebSocket server.
* @param {String} command command to send
* @param {String} data data to send
*/
function sendCommand(command, data) {
if (window.ws.readyState === WebSocket.OPEN) {
window.ws.send(JSON.stringify({ command, data }));
}
}
/**
* Fetch API helper function
* @param {string} endpoint Endpoint URL
* @param {string} method Method (GET, POST, etc.)
* @param {Object} headers Headers to include in the request
* @param {Object} body Body of the request
* @param {Function} cbOK Callback function for successful response
* @param {Function} cbError Callback function for error response
*/
function fetchAPI(endpoint, method, headers = {}, body = null, cbOK, cbError) {
let url = window.location.origin + "/api/" + endpoint;
let options = {
method: method,
headers: headers
}
if (body !== null) {
options.body = JSON.stringify(body);
if (!options.headers['Content-Type']) {
options.headers['Content-Type'] = 'application/json';
}
}
fetch(url, options)
.then(async (response) => {
if (!response.ok) {
let msg;
try {
let _xxx = await response.json();
msg = _xxx.message || response.statusText;
} catch {
msg = await response.statusText;
}
throw new Error(msg);
}
return response.json();
})
.then(data => {
cbOK(data);
})
.catch(error => {
cbError(error);
});
}
/**
* Fetch asset file from /assets/img/*
* @param {String} url the filename to fetch, relative to /assets/img/
* @param {Function} cbOK callback function on success, will receive the object URL
* @param {Function} cbError callback function on error, will receive the error object
*/
function fetchImg(url, cbOK, cbError) {
url = "/assets/img/" + url;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
return response.blob();
})
.then(blob => {
const url = URL.createObjectURL(blob);
cbOK(url);
})
.catch(error => {
cbError(error);
});
}
/**
* Reload voice types from server
*/
function getVoiceTypes() {
window.voiceTypes = [];
fetchAPI("VoiceType", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
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) => {
alert("Error loading voice types : " + errdata.message);
});
}
/**
* Reload categories from server
*/
function getCategories() {
window.categories = [];
fetchAPI("Category", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
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) => {
alert("Error loading categories : " + errdata.message);
});
}
/**
* Reload languages from server
*/
function getLanguages() {
window.languages = [];
fetchAPI("Language", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
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) => {
alert("Error loading languages : " + errdata.message);
});
}
/**
* Reload scheduled days from server
*/
function getScheduledDays() {
window.scheduledays = [];
fetchAPI("ScheduleDay", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
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) => {
alert("Error loading scheduled days : " + errdata.message);
});
}
/**
* Clear database mechanism
* @param {String} APIURL API URL endpoint
* @param {String} whattoclear what to clear
* @param {Function} cbOK callback function on success
* @param {Function} cbError callback function on error
*/
function DoClear(APIURL, whattoclear, cbOK, cbError) {
if (confirm(`Are you sure want to clear ${whattoclear} ? This procedure is not reversible`)) {
fetchAPI(APIURL + "List", "DELETE", {}, null, (okdata) => {
cbOK(okdata);
}, (errdata) => {
cbError(errdata);
});
}
}
/**
* Export mechanism to XLSX file
* @param {String} APIURL API URL endpoint
* @param {String} filename target filename
* @param {Object} queryParams additional query parameters as object
*/
function DoExport(APIURL, filename, queryParams = {}) {
// send GET request to APIURL + "ExportXLSX"
// reply Content-Type is application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
// reply Content-Disposition: attachment; filename=filename
// Use fetch to download the XLSX file as a blob and trigger download
let url = "/api/" + APIURL + "ExportXLSX";
if (queryParams && Object.keys(queryParams).length > 0) {
url += "?" + new URLSearchParams(queryParams).toString();
}
fetch(url, {
method: "GET",
headers: {}
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok ' + response.statusText);
return response.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
})
.catch(error => {
alert("Error export to " + filename + ": " + error.message);
});
return; // prevent the rest of the function from running
}
/**
* Import mechanism from XLSX file
* @param {String} APIURL API URL endpoint
* @param {Function<Object>} cbOK function that accept object data
* @param {Function<Error>} cbError function that accept error object
*/
function DoImport(APIURL, cbOK, cbError) {
// Open file selection dialog that accepts only .xlsx files
// then upload to server using fetchAPI at "api/ImportXLSX" with POST method
let fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.xlsx';
fileInput.onchange = e => {
let file = e.target.files[0];
if (file) {
let formData = new FormData();
formData.append('file', file);
fetch("/api/" + APIURL + "ImportXLSX", { method: 'POST', body: formData })
.then(response => {
if (!response.ok) { throw new Error('Network response was not ok ' + response.statusText); }
return response.json();
})
.then(data => {
cbOK(data);
})
.catch(error => {
cbError(error);
});
} else {
cbError(new Error("No file selected"));
}
};
fileInput.click();
fileInput.remove();
}
window.greencircle = null;
window.redcircle = null;
/**
* App entry point
*/
$(document).ready(function () {
document.title = "Automatic Announcement System"
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;
}
// reset status indicators
function resetStatusIndicators() {
$('#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");
}
resetStatusIndicators();
getVoiceTypes();
getCategories();
getLanguages();
getScheduledDays();
// reconnect handle
let ws_reconnect;
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', 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)
break;
case "getMemoryStatus":
$('#ramstatus').text("RAM : " + data)
break;
case "getDiskStatus":
$('#diskstatus').text("Disk : " + data)
break;
case "getNetworkStatus":
let result = "";
let json = JSON.parse(data);
if (Array.isArray(json) && json.length > 0) {
json.forEach((net) => {
if (result.length > 0) result += "\n"
result += `${net.displayName} (${net.ipV4addr.join(";")}) TX:${(net.txSpeed / 1024).toFixed(1)} KB/s RX:${(net.rxSpeed / 1024).toFixed(1)} KB/s`
})
} else result = "N/A";
$('#networkstatus').text(result)
break;
case "getSystemTime":
$('#datetimetext').text(data)
break;
}
}
});
window.ws = s;
}
reconnect();
window.addEventListener('beforeunload', () => {
try{
window.ws?.close(1000, "Client closed connection");
} catch (error) {
console.error("Error closing WebSocket connection:", error);
}
});
setInterval(() => {
sendCommand("getCPUStatus", "")
sendCommand("getMemoryStatus", "")
sendCommand("getDiskStatus", "")
sendCommand("getNetworkStatus", "")
sendCommand("getSystemTime", "")
}, 1000)
let sidemenu = new bootstrap.Offcanvas('#offcanvas-menu');
$('#showmenu').click(() => { sidemenu.show(); })
$('#homelink').click(() => {
sidemenu.hide();
$('#content').load('overview.html', function (response, status, xhr) {
if (status === "success") {
console.log("Overview content loaded successfully");
}
});
});
$('#soundbanklink').click(() => {
sidemenu.hide();
$('#content').load('soundbank.html', function (response, status, xhr) {
if (status === "success") {
console.log("Soundbank content loaded successfully");
// pindah soundbank.js
} else {
console.error("Error loading soundbank content : ", xhr.status, xhr.statusText);
}
});
})
$('#messagebanklink').click(() => {
sidemenu.hide();
$('#content').load('messagebank.html', function (response, status, xhr) {
if (status === "success") {
console.log("Messagebank content loaded successfully");
// pindah messagebank.js
} else {
console.error("Error loading messagebank content : ", xhr.status, xhr.statusText);
}
});
})
$('#languagelink').click(() => {
sidemenu.hide();
$('#content').load('language.html', function (response, status, xhr) {
if (status === "success") {
console.log("Language content loaded successfully");
// pindah languagelink.js
} else {
console.error("Error loading language content : ", xhr.status, xhr.statusText);
}
});
})
$('#broadcastzonelink').click(() => {
sidemenu.hide();
$('#content').load('broadcastzones.html', function (response, status, xhr) {
if (status === "success") {
console.log("Broadcast Zone content loaded successfully");
// pindah ke broadcastzones.js
} else {
console.error("Error loading broadcast zone content : ", xhr.status, xhr.statusText);
}
});
})
$('#timerlink').click(() => {
sidemenu.hide();
$('#content').load('timer.html', function (response, status, xhr) {
if (status === "success") {
console.log("Timer content loaded successfully");
// pindah ke schedulebank.js
} else {
console.error("Error loading timer content : ", xhr.status, xhr.statusText);
}
});
})
$('#loglink').click(() => {
sidemenu.hide();
$('#content').load('log.html', function (response, status, xhr) {
if (status === "success") {
console.log("Log content loaded successfully");
// pindah ke log.js
} else {
console.error("Error loading log content:", xhr.status, xhr.statusText);
}
});
})
$('#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) {
if (status === "success") {
console.log("Setting content loaded successfully");
//sendCommand("getSetting", "");
} else {
console.error("Error loading setting content:", xhr.status, xhr.statusText);
}
});
})
$('#logoutlink').click(() => {
window.location.href = "login.html"
})
});

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,432 @@
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* @typedef {Object} SoundBank
* @property {number} index
* @property {string} description
* @property {string} tag
* @property {string} category
* @property {string} language
* @property {string} voiceType
* @property {string} path
*/
/**
* List of Soundbank data loaded from server
* @type {SoundBank[]}
*/
window.soundbankdata = [];
/**
* Currently selected soundbank row in the table
* @type {JQuery<HTMLElement>|null}
*/
window.selectedsoundrow = null;
/**
* Select2 data source
* See https://select2.org/data-sources/formats
* @type {Select2item[]}
*/
window.select2data = [];
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
*/
function reloadSoundBank(APIURL = "SoundBank/") {
window.soundbankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.soundbankdata.push(...okdata);
window.selectedsoundrow = null;
fill_soundbanktablebody(window.soundbankdata);
}
}, (errdata) => {
alert("Error loading soundbank : " + errdata.message);
});
}
/**
* Fill soundbank table body with values
* @param {SoundBank[]} vv values to fill
*/
function fill_soundbanktablebody(vv) {
$('#soundbanktablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.description}</td>
<td>${item.tag}</td>
<td>${item.category}</td>
<td>${item.language}</td>
<td>${item.voiceType}</td>
<td>${item.path}</td>
</tr>`;
$('#soundbanktablebody').append(row);
let $addedrow = $('#soundbanktablebody tr:last');
$addedrow.on('click', function () {
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');
window.selectedsoundrow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload soundbank files from server and filter by language, category, and voiceType
* @param {String} language
* @param {String} category
* @param {String} voiceType
* @param {Function} cb callback function when done
*/
function reloadSoundbankFiles(language, category, voiceType, cb=null) {
window.select2data = [];
$('#modalpath').empty().trigger('change');
if (language && language.length > 0) {
if (category && category.length > 0) {
if (voiceType && voiceType.length > 0) {
fetchAPI(`ListFiles/${language}/${voiceType}/${category}`, "GET", {}, null, (okdata) => {
console.log("reloadSoundbankFiles: got " + okdata.length + " items");
if (Array.isArray(okdata)){
window.select2data = okdata.map(p => ({id: getFilenameFromPath(p), text: getFilenameFromPath(p)}));
$('#modalpath').select2({
data: window.select2data,
placeholder: 'Select a sound file',
allowClear: true,
width: '100%',
dropdownParent: $('#soundbankmodal')
});
if (cb) cb();
}
}, (errdata) => {
alert("Error loading soundbank files : " + errdata.message);
});
}
}
}
}
function getFilenameFromPath(path) {
if (!path || path.length === 0) return "";
if (path.includes('\\')) {
let parts = path.split('\\');
return parts[parts.length - 1];
} else if (!path.includes('/')) {
let parts = path.split('/');
return parts[parts.length - 1];
}
}
$(document).ready(function () {
console.log("soundbank.js loaded successfully");
$('#soundbanktablebody').empty();
window.selectedsoundrow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
$btnRemove.prop('disabled', true);
$btnEdit.prop('disabled', true);
let APIURL = "SoundBank/";
let $modal = $('#soundbankmodal');
let $modalindex = $modal.find('#modalindex');
let $modaldescription = $modal.find('#modaldescription');
let $modaltag = $modal.find('#modaltag');
let $modalcategory = $modal.find('#modalcategory');
let $modallanguage = $modal.find('#modallanguage');
let $modalvoicetype = $modal.find('#modalvoicetype');
let selected_category = null;
let selected_language = null;
let selected_voicetype = null;
/**
* Clear soundbank modal inputs
*/
function clearSoundbankModal() {
$modalindex.val('').prop('disabled', true);
$modaldescription.val('');
$modaltag.val('');
// fill modalcategory options from categories[]
$modalcategory.empty();
categories.forEach(cat => {
$modalcategory.append(new Option(cat, cat));
});
$modalcategory.val(null);
// fill modallanguage options from languages[]
$modallanguage.empty();
languages.forEach(lang => {
$modallanguage.append(new Option(lang, lang));
});
$modallanguage.val(null);
// fill modalvoicetype options from voiceTypes[]
$modalvoicetype.empty();
voiceTypes.forEach(vt => {
$modalvoicetype.append(new Option(vt, vt));
});
$modalvoicetype.val(null);
$('#modalpath').select2()
}
reloadSoundBank(APIURL);
$('#findsoundbank').on('input', function () {
let searchTerm = $(this).val().trim().toLowerCase();
if (searchTerm.length > 0) {
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 {
window.selectedsoundrow = null;
fill_soundbanktablebody(window.soundbankdata);
}
});
$btnClear.click(() => {
DoClear(APIURL, "Soundbank", (okdata) => {
reloadSoundBank(APIURL);
alert("Success clear soundbank : " + okdata.message);
}, (errdata) => {
alert("Error clear soundbank : " + errdata.message);
});
});
function SetupEventForCategoryLanguageVoiceType() {
$modalcategory.off('change').on('change', function () {
selected_category = $(this).val();
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype);
});
$modallanguage.off('change').on('change', function () {
selected_language = $(this).val();
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype);
});
$modalvoicetype.off('change').on('change', function () {
selected_voicetype = $(this).val();
reloadSoundbankFiles(selected_language, selected_category, selected_voicetype);
});
}
$btnAdd.click(() => {
$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 () {
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 () {
$modal.modal('hide');
});
});
$btnRemove.click(() => {
if (window.selectedsoundrow) {
let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */
let sb = {
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 delete soundbank [${sb.index}] Description=${sb.description} Tag=${sb.tag}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sb.index, "DELETE", {}, null, (okdata) => {
reloadSoundBank(APIURL);
alert("Success delete soundbank : " + okdata.message);
}, (errdata) => {
alert("Error delete soundbank : " + errdata.message);
});
}
}
});
$btnEdit.click(() => {
if (window.selectedsoundrow) {
let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */
let sb = {
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}?`)) {
$modal.modal('show');
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 () {
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');
});
}
}
});
$btnExport.click(() => {
DoExport(APIURL, "soundbank.xlsx", {});
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadSoundBank(APIURL);
alert("Success import soundbank : " + okdata.message);
}, (errdata) => {
alert("Error importing soundbank from XLSX : " + errdata.message);
});
});
});

View File

@@ -0,0 +1,185 @@
/**
* @typedef {Object} SoundChannel
* @property {number} index - The index of the sound channel.
* @property {string} channel - The name of the sound channel.
* @property {string} ip - The IP address associated with the sound channel.
*/
/**
* @type {SoundChannel[]}
*/
window.soundChannels = [];
// Currently selected sound channel row in the table
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) {
let $btnEditSoundChannel = $('#btnEditSoundChannel');
let $tablesizeSoundChannel = $('#tablesizeSoundChannel');
$('#soundchanneltablebody').empty();
$tablesizeSoundChannel.text('Table Length : N/A');
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.channel}</td>
<td>${item.ip}</td>
</tr>`;
$('#soundchanneltablebody').append(row);
let $addedrow = $('#soundchanneltablebody tr:last');
$addedrow.off('click').on('click', function () {
if (selectedSoundChannel) {
selectedSoundChannel.find('td').css('background-color', '');
if (selectedSoundChannel.is($(this))) {
selectedSoundChannel = null;
$btnEditSoundChannel.prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
selectedSoundChannel = $(this);
$btnEditSoundChannel.prop('disabled', false);
});
});
$tablesizeSoundChannel.text("Table Size: " + vv.length);
}
/**
* Reload sound channels from server
* @param {String} APIURL API URL endpoint (default "SoundChannel/")
*/
function reloadSoundChannel(APIURL = "SoundChannel/") {
window.soundChannels = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadSoundChannel : ", okdata)
window.soundChannels.push(...okdata);
fill_soundchanneltablebody(window.soundChannels);
} else console.log("reloadSoundChannel: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
$(document).ready(function () {
console.log("soundchannel.js loaded successfully");
let $soundchannelmodal = $('#soundchannelmodal');
let $soundchannelindex = $soundchannelmodal.find('#soundchannelindex');
let $soundchanneldescription = $soundchannelmodal.find('#soundchanneldescription');
let $soundchannelip = $soundchannelmodal.find('#soundchannelip');
let $btnReinitializeSoundChannel = $('#btnReinitializeSoundChannel');
let $btnEditSoundChannel = $('#btnEditSoundChannel');
let $btnExportSoundChannel = $('#btnExportSoundChannel');
let $btnImportSoundChannel = $('#btnImportSoundChannel');
let $findsoundchannel = $('#findsoundchannel');
$btnEditSoundChannel.prop('disabled', true);
let API_SoundChannel = "SoundChannel/";
$findsoundchannel.off('input').on('input', function () {
let searchTerm = $(this).val().toLowerCase();
if (searchTerm.length==0){
window.selectedSoundChannel = null;
fill_soundchanneltablebody(window.soundChannels);
} else {
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);
}
});
/**
* Clear sound channel modal inputs
*/
function clearSoundChannelModal() {
$soundchannelindex.val('');
$soundchanneldescription.val('');
$soundchannelip.val('');
}
reloadSoundChannel(API_SoundChannel);
$btnReinitializeSoundChannel.off('click').on('click', () => {
DoClear(API_SoundChannel, "SoundChannels", (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success clear sound channels: " + okdata.message);
}, (errdata) => {
alert("Error clear sound channels: " + errdata.message);
});
});
$btnEditSoundChannel.off('click').on('click', () => {
if (selectedSoundChannel) {
let cells = selectedSoundChannel.find('td');
/** @type {SoundChannel} */
let sc = {
index: parseInt(cells.eq(0).text(), 10),
description: cells.eq(1).text(),
ip: cells.eq(2).text()
};
if (confirm(`Are you sure to edit sound channel [${sc.index}] Description=${sc.description} IP=${sc.ip}?`)) {
$soundchannelmodal.modal('show');
clearSoundChannelModal();
$soundchannelindex.val(sc.index).prop('disabled', true);
$soundchanneldescription.val(sc.description);
$soundchannelip.val(sc.ip);
// Handle save changes
$soundchannelmodal.off('click.soundchannelsave').on('click.soundchannelsave', '#soundchannelsave', function () {
let newsc = {
index: parseInt($soundchannelindex.val(), 10),
description: $soundchanneldescription.val(),
ip: $soundchannelip.val()
};
if (newsc.description.trim().length === 0) {
alert("Description cannot be empty");
return;
}
if (newsc.ip.trim().length === 0) {
alert("IP cannot be empty");
return;
}
if (newsc.description===sc.description && newsc.ip===sc.ip){
alert("No changes detected");
return;
}
fetchAPI(API_SoundChannel + "UpdateByIndex/" + newsc.index, "PATCH", {}, newsc, (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success edit sound channel: " + okdata.message);
}, (errdata) => {
alert("Error edit sound channel: " + errdata.message);
});
$soundchannelmodal.modal('hide');
});
$soundchannelmodal.off('click.soundchannelclose').on('click.soundchannelclose', '#soundchannelclose', function () {
$soundchannelmodal.modal('hide');
});
}
}
});
$btnExportSoundChannel.off('click').on('click', () => {
DoExport(API_SoundChannel, "soundchannels.xlsx", {});
});
$btnImportSoundChannel.off('click').on('click', () => {
DoImport(API_SoundChannel, (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success import sound channels: " + okdata.message);
}, (errdata) => {
alert("Error importing sound channels from XLSX: " + errdata.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

@@ -0,0 +1,291 @@
<!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>
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">Sound Channel and Broadcast Zones</h2>
</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 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 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 form-control input-add" type="text" id="broadcastzoneindex" placeholder="index" 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">Description</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Channel</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Box</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Relay</p>
</div>
<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">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R01"><label class="form-check-label" for="formCheck-1">01</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R02"><label class="form-check-label" for="formCheck-2">02</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R03"><label class="form-check-label" for="formCheck-7">03</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R04"><label class="form-check-label" for="formCheck-6">04</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R05"><label class="form-check-label" for="formCheck-5">05</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R06"><label class="form-check-label" for="formCheck-4">06</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R07"><label class="form-check-label" for="formCheck-3">07</label></div>
</div>
<div class="row">
<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 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>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R10"><label class="form-check-label" for="formCheck-26">10</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R11"><label class="form-check-label" for="formCheck-27">11</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R12"><label class="form-check-label" for="formCheck-28">12</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R13"><label class="form-check-label" for="formCheck-29">13</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R14"><label class="form-check-label" for="formCheck-30">14</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R15"><label class="form-check-label" for="formCheck-31">15</label></div>
</div>
<div class="row">
<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 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>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R18"><label class="form-check-label" for="formCheck-18">18</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R19"><label class="form-check-label" for="formCheck-19">19</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R20"><label class="form-check-label" for="formCheck-20">20</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R21"><label class="form-check-label" for="formCheck-21">21</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R22"><label class="form-check-label" for="formCheck-22">22</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R23"><label class="form-check-label" for="formCheck-23">23</label></div>
</div>
<div class="row">
<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 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>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R26"><label class="form-check-label" for="formCheck-10">26</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R27"><label class="form-check-label" for="formCheck-11">27</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R28"><label class="form-check-label" for="formCheck-12">28</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R29"><label class="form-check-label" for="formCheck-13">29</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R30"><label class="form-check-label" for="formCheck-14">30</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R31"><label class="form-check-label" for="formCheck-15">31</label></div>
</div>
<div class="row">
<div class="form-check"><input class="form-check-input" type="checkbox" id="R32"><label class="form-check-label" for="formCheck-16">32</label></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="broadcastzoneclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="broadcastzonesave" type="button">Save</button></div>
</div>
</div>
</div>
<div class="row">
<div class="accordion" role="tablist" id="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>
<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="findsoundchannel" placeholder="Search keyword" name="findsoundbank"></div>
</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">
<p id="tablesizeSoundChannel" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="class05">No</th>
<th class="class75">Description</th>
<th class="class20">IP Address</th>
</tr>
</thead>
<tbody id="soundchanneltablebody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<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>
<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="findzone" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<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>
<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="col">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<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>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="soundchannelmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<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 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-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 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</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 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">IP Address</p>
</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-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>
</html>

108
html/webpage/home.html Normal file
View File

@@ -0,0 +1,108 @@
<!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 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>
<body>
<div class="text-dark bg-light pad-header bg-header" id="header_home">
<div class="row">
<div class="col-2 col-sm-2 col-md-1 col-lg-1 col-xl-1"><button class="btn btn-primary h-100 bread-menu" id="showmenu" type="button"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" style="width: 32px;height: 32px;">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path>
</svg></button></div>
<div class="col-8 col-sm-8 col-md-10 col-lg-10 col-xl-10 col-xxl-10">
<h1 class="text-header" style="text-align: left;">Automatic Announcement System</h1>
</div>
<div class="col-2 col-sm-2 col-md-1 col-lg-1 col-xl-1 col-xxl-1 offset-xxl-0 align-items-end align-ite"><img id="onlineindicator" class="img-indicator" src="assets/img/red_circle.png" width="24" height="24"></div>
</div>
</div>
<div class="offcanvas offcanvas-start bg-body" tabindex="-1" data-bs-backdrop="false" id="offcanvas-menu">
<div class="offcanvas-header"><a class="link-body-emphasis d-flex align-items-center me-md-auto mb-3 mb-md-0 text-decoration-none" href="/"><img src="assets/img/logogtc-grey.png" width="48" height="48"><span class="fs-4 font-top-menu">AAS v.2&nbsp;</span></a><button class="btn-close" type="button" aria-label="Close" data-bs-dismiss="offcanvas"></button></div>
<div class="offcanvas-body d-flex flex-column justify-content-between pt-0">
<div>
<hr class="mt-0">
<ul class="nav nav-pills flex-column mb-auto">
<li class="nav-item"><a class="nav-link active link-light text-menu" id="homelink" href="#" aria-current="page"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-house-door me-2" style="font-size: 20px;">
<path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4z"></path>
</svg>&nbsp;Overview</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="soundbanklink" href="#"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-soundwave me-2 icon-menu" style="font-size: 20px;">
<path fill-rule="evenodd" d="M8.5 2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-1 0v-11a.5.5 0 0 1 .5-.5m-2 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5m4 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5m-6 1.5A.5.5 0 0 1 5 6v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m8 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m-10 1A.5.5 0 0 1 3 7v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5m12 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5"></path>
</svg>&nbsp;Sound Bank</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="messagebanklink" 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="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"></path>
</svg>&nbsp;Message Bank</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="languagelink" 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="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"></path>
</svg>&nbsp;Language Link</a></li>
<li class="nav-item .icon-menu"><a class="nav-link link-body-emphasis text-menu" id="timerlink" 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="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"></path>
<path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path>
</svg>&nbsp;Timer</a></li>
<li class="nav-item .icon-menu"><a class="nav-link link-body-emphasis text-menu" id="broadcastzonelink" 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="M18.2 1H9.8C8.81 1 8 1.81 8 2.8v14.4c0 .99.81 1.79 1.8 1.79l8.4.01c.99 0 1.8-.81 1.8-1.8V2.8c0-.99-.81-1.8-1.8-1.8zM14 3c1.1 0 2 .89 2 2s-.9 2-2 2-2-.89-2-2 .9-2 2-2zm0 13.5c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"></path>
<circle cx="14" cy="12.5" r="2.5"></circle>
<path d="M6 5H4v16c0 1.1.89 2 2 2h10v-2H6V5z"></path>
</svg>&nbsp;Broadcast Zones</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="loglink" 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="icon-menu" style="font-size: 20px;">
<g>
<rect fill="none" height="24" width="24"></rect>
<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>
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"></path>
</g>
</svg>&nbsp;Setting</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="logoutlink" 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="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"></path>
</svg>&nbsp;Logout</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col card-status">
<p class="w-100 h-100 text-status" id="cpustatus">CPU Status :&nbsp;<br><br></p>
</div>
<div class="col card-status">
<p class="w-100 h-100 text-status" id="ramstatus">RAM Status :&nbsp;<br><br></p>
</div>
<div class="col card-status">
<p class="w-100 h-100 text-status" id="diskstatus">Disk Status :&nbsp;<br><br></p>
</div>
<div class="col card-status">
<p class="w-100 h-100 text-status" id="networkstatus">Network&nbsp;</p>
</div>
<div class="col card-status">
<p class="w-100 h-100 text-status" id="datetimetext">Date and Time&nbsp;</p>
</div>
</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/script.js"></script>
<script src="assets/js/select2.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,96 @@
<!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>
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">Language Link</h2>
</div>
</div>
<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>
</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="findlanguage" 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>
<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="col">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="class05">No</th>
<th class="class20">TAG</th>
<th class="class75">Languages</th>
</tr>
</thead>
<tbody id="languagebanktablebody"></tbody>
</table>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="languagemodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<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 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 form-control input-add" type="text" id="languagelinkindex" 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">Tag</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</div>
<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>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langJap" name="languages[]" value="jap"><label class="form-check-label" for="langJap">Japanese</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langChi" name="languages[]" value="chi"><label class="form-check-label" for="langChi">Chinese</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langArb" name="languages[]" value="arb"><label class="form-check-label" for="langArb">Arabic</label></div>
</div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="languagelinkclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="languagelinksave" 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/languagelink.js"></script>
</body>
</html>

58
html/webpage/log.html Normal file
View File

@@ -0,0 +1,58 @@
<!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>
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">Operational Log</h2>
</div>
</div>
<div class="row">
<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-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-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">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<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>
</table>
</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>
</html>

43
html/webpage/login.html Normal file
View File

@@ -0,0 +1,43 @@
<!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 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>
<body>
<section class="position-relative py-4 py-xl-5">
<div class="container">
<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 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>
<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>
</div>
</div>
</div>
</section>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
</body>
</html>

View File

@@ -0,0 +1,138 @@
<!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>
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">Message Bank</h2>
</div>
</div>
<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">
<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 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>
<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="col">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<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>
</table>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Message</h4>
</div>
<div class="modal-body bg-modal-body">
<div class="row">
<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-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-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Description</p>
</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-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Language</p>
</div>
<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>
<option value="JAPANESE">Japanese</option>
<option value="CHINESE">Chinese</option>
<option value="ARABIC">Arabic</option>
</select></div>
</div>
<div class="row">
<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-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-4 col-md-4 col-lg-3 col-xl-3">
<p class="text-add">Voice Type</p>
</div>
<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 bg-light">
<ul class="list-unstyled w-100 h-100" id="messageavailablevariables"></ul>
</div>
<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 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 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 bg-light">
<ul class="list-unstyled w-100 h-100" id="messageselectedvariables"></ul>
</div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" 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/messagebank.js"></script>
</body>
</html>

1338
html/webpage/overview.html Normal file

File diff suppressed because it is too large Load Diff

86
html/webpage/setting.html Normal file
View File

@@ -0,0 +1,86 @@
<!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>
<div class="row">
<div class="col w-100 h-100 pad-header">
<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>

117
html/webpage/soundbank.html Normal file
View File

@@ -0,0 +1,117 @@
<!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;">Sound Bank</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="findsoundbank" 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="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>
</table>
</div>
</div>
<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 bg-header-modal">
<h4 class="modal-title align-content-center">Add / Edit Soundbank</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">Description</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">TAG</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Category</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Voice Type</p>
</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">
<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-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>
</div>
</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>
</html>

View File

@@ -0,0 +1,36 @@
<!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>
<div class="card" id="streamercard">
<div class="card-body card-channel">
<h4 class="card-title" id="streamertitle">Channel 01</h4>
<div class="row">
<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-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>
<p class="card-text" id="streamerstatus">Status : Idle</p>
<div class="progress" id="streamervu">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
</div>
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
</body>
</html>

181
html/webpage/timer.html Normal file
View File

@@ -0,0 +1,181 @@
<!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>
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">Schedule Bank</h2>
</div>
</div>
<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>
</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="findschedule" 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>
<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="col">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<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>
</table>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="schedulemodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-header-modal">
<h4 class="modal-title">Add / Edit Schedule</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="scheduleid" 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">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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Day</p>
</div>
<div class="col">
<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 pad-day">
<div class="col">
<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-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-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-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-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-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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Message</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="schedulemessage" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</div>
<div class="col">
<div class="row pad-day">
<div class="col"><select class="w-100 input-add form-select" id="languageselect">
<optgroup label="This is a group">
<option value="12" selected="">This is item 1</option>
<option value="13">This is item 2</option>
<option value="14">This is item 3</option>
</optgroup>
</select></div>
</div>
</div>
</div>
<div class="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-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-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Enable</p>
</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-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-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>
</div>
</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>
</html>

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>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/win32-x86/bassenc.dll Normal file

Binary file not shown.

View File

@@ -1,11 +1,243 @@
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
import database.MariaDB
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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()
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
// dipakai untuk ambil messagebank berdasarkan id
val urutan_bahasa = listOf(
Language.INDONESIA.name,
Language.LOCAL.name,
Language.ENGLISH.name,
Language.CHINESE.name,
Language.JAPANESE.name,
Language.ARABIC.name
)
val audioPlayer = AudioPlayer()
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() {
Logger.info("Application started" as Any)
if (Platform.isWindows()) {
// supaya OSHI bisa mendapatkan CPU usage di Windows seperti di Task Manager
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()
// Coroutine untuk cek Paging Queue dan AAS Queue setiap detik
CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
delay(1000)
// prioritas 1 , habisin queue paging
subcode01.Read_Queue_Paging()
// 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
CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
delay(1000)
subcode01.Read_Schedule_Table()
}
}
val web = WebApp(
3030,
listOf(
Pair("admin", "password"),
Pair("user", "password")
))
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 ->
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}" }
if (_sc != null) {
val _bc = BarixConnection(_sc.index, _sc.channel, cmd.ipaddress)
// 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 {
// sudah ada, update data
if (_sc != null && _sc.channel != _streamer.channel) {
_streamer.channel = _sc.channel
}
// 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
}
}
}
}
val onlinechecker = fixedRateTimer(name = "onlinecheck", initialDelay = 1000, period = 1000) {
// cek setiap 1 detik, decrement online counter semua BarixConnection
StreamerOutputs.values.forEach {
it.decrementOnlineCounter()
}
}
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()
})
}

1120
src/MainExtension01.kt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
package audio
/**
* Class used for storing audio file information.
*/
class AudioFileInfo {
var fileName : String = ""
var fileSize : Long = 0L
var duration : Double = 0.0
var bytes : ByteArray = ByteArray(0)
/**
* Check if the audio file information is valid.
* A valid audio file must have a non-blank file name, a positive file size,
* a positive duration, and non-empty byte array.
* @return True if the audio file information is valid, false otherwise.
*/
fun isValid() : Boolean {
return fileName.isNotBlank() && fileSize > 0 && duration > 0.0 && bytes.isNotEmpty()
}
}

View File

@@ -1,13 +1,33 @@
package audio
import audio.Bass.BASS_ERROR_ALREADY
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.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 com.sun.jna.Pointer
import contentCache
import org.tinylog.Logger
class AudioPlayer {
var bass : Bass = Bass.Instance
var initedDevice : Int = -1
@Suppress("unused")
class AudioPlayer (var samplingrate: Int) {
val bass: Bass = Bass.Instance
val bassenc : BassEnc = BassEnc.Instance
var initedDevice = -1
init {
Logger.info("Audio version ${Integer.toHexString(bass.BASS_GetVersion())}" as Any)
if (samplingrate<1) samplingrate = 44100 // Default sampling rate
Logger.info {"Bass version ${Integer.toHexString(bass.BASS_GetVersion())}"}
Logger.info { "BassEnc version ${Integer.toHexString(bassenc.BASS_Encode_GetVersion())}" }
InitAudio(0) // Audio 0 is No Sound, use for reading and writing wav silently
}
@@ -19,6 +39,22 @@ class AudioPlayer {
*/
fun InitAudio(id : Int) : Boolean {
val bdi = Bass.BASS_DEVICEINFO()
if (bass.BASS_GetDeviceInfo(id, bdi)){
Logger.info { "Audio ID=$id Name=${bdi.name} Driver=${bdi.driver}" }
if (bdi.flags and BASS_DEVICE_ENABLED == 0) {
Logger.error { "Audio ID=$id is not enabled, cannot initialize" }
return false
}
if (bdi.flags and BASS_DEVICE_INIT > 0){
Logger.info { "Audio ID=$id is already initialized" }
if (id > 0) {
initedDevice = id
Logger.info { "Real Audio Device reused ID=$initedDevice" }
}
return true
}
}
if (bass.BASS_Init(id, 48000, 0)){
Logger.info { "Audio ID=$id inited succesfully" }
@@ -28,26 +64,15 @@ class AudioPlayer {
}
return true
} else {
val err = bass.BASS_ErrorGetCode()
if (err == BASS_ERROR_ALREADY) {
Logger.info {"Audio ID=$id already initialized, reusing existing instance"}
if (id > 0) {
initedDevice = id
Logger.info { "Real Audio Device reused ID=$initedDevice" }
}
return true
} else {
Logger.error { "Audio ID=$id initialization failed: ${bass.BASS_ErrorGetCode()}" }
return false
}
Logger.error { "Audio ID=$id initialization failed: ${bass.BASS_ErrorGetCode()}" }
return false
}
}
/**
* Uninitializes the audio system if it was previously initialized.
*/
fun UnInitAudio(){
fun Close(){
if (initedDevice != -1) {
bass.BASS_SetDevice(initedDevice)
if (bass.BASS_Free()) {
@@ -58,4 +83,227 @@ class AudioPlayer {
initedDevice = -1
}
}
/**
* Loads an audio file and retrieves its information.
* Check for isValid() method in AudioFileInfo class to verify the loaded file.
* @param fileName The name of the audio file to load.
* @return An AudioFileInfo object containing the file information.
*/
fun LoadAudioFile(fileName: String) : AudioFileInfo {
val result = AudioFileInfo()
if (ValidFile(fileName)){
result.fileName = fileName
bass.BASS_SetDevice(0) // Set to No Sound device for reading
val handle = bass.BASS_StreamCreateFile(false, fileName, 0L, 0L, BASS_STREAM_DECODE or BASS_SAMPLE_MONO)
if (handle!=0){
// successfully opened the file
// read file size
val size = bass.BASS_ChannelGetLength(handle, BASS_POS_BYTE)
if (size > 0) {
result.fileSize = size
}
// read file duration
val duration = bass.BASS_ChannelBytes2Seconds(handle, size)
if (duration > 0) {
result.duration = duration
}
// read file bytes
val mem = Memory(size)
val bytesRead = bass.BASS_ChannelGetData(handle, mem, size.toInt())
if (bytesRead > 0) {
result.bytes = mem.getByteArray(0, bytesRead)
}
// close the handle
bass.BASS_StreamFree(handle)
}
}
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 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, withChime: Boolean = true) : Result_Boolean_String {
if (sources.isEmpty()) {
return Result_Boolean_String(false,"Invalid Source")
}
if (!ValidString(target)) {
return Result_Boolean_String(false, " Invalid target file name")
}
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){
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)
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
if (!pushData(source.bytes)){
allsuccess = false
Logger.error { "Source ${source.fileName} push failed" }
}
} 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)
return if (allsuccess){
Result_Boolean_String(true, "WAV file written successfully: $target")
} else {
Result_Boolean_String(false, "Failed to write some data to WAV file: $target")
}
}
}

View File

@@ -416,6 +416,8 @@ public interface Bass extends Library {
int BASS_SYNC_MIXTIME = 0x40000000; // flag: sync at mixtime, else at playtime
int BASS_SYNC_ONETIME = 0x80000000; // flag: sync only once, else continuously
interface SYNCPROC extends Callback
{
void SYNCPROC(int handle, int channel, int data, Pointer user);
@@ -714,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);
@@ -783,8 +786,6 @@ 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);

130
src/audio/BassEnc.java Normal file
View File

@@ -0,0 +1,130 @@
package audio;
import com.sun.jna.*;
@SuppressWarnings("unused")
public interface BassEnc extends Library {
BassEnc Instance = (BassEnc) Native.load("bassenc", BassEnc.class);
// Additional error codes returned by BASS_ErrorGetCode
int BASS_ERROR_CAST_DENIED = 2100; // access denied (invalid password)
int BASS_ERROR_SERVER_CERT = 2101; // missing/invalid certificate
// Additional BASS_SetConfig options
int BASS_CONFIG_ENCODE_PRIORITY = 0x10300;
int BASS_CONFIG_ENCODE_QUEUE = 0x10301;
int BASS_CONFIG_ENCODE_CAST_TIMEOUT = 0x10310;
// Additional BASS_SetConfigPtr options
int BASS_CONFIG_ENCODE_CAST_PROXY = 0x10311;
int BASS_CONFIG_ENCODE_CAST_BIND = 0x10312;
int BASS_CONFIG_ENCODE_SERVER_CERT = 0x10320;
int BASS_CONFIG_ENCODE_SERVER_KEY = 0x10321;
// BASS_Encode_Start flags
int BASS_ENCODE_NOHEAD = 1; // don't send a WAV header to the encoder
int BASS_ENCODE_FP_8BIT = 2; // convert floating-point sample data to 8-bit integer
int BASS_ENCODE_FP_16BIT = 4; // convert floating-point sample data to 16-bit integer
int BASS_ENCODE_FP_24BIT = 6; // convert floating-point sample data to 24-bit integer
int BASS_ENCODE_FP_32BIT = 8; // convert floating-point sample data to 32-bit integer
int BASS_ENCODE_FP_AUTO = 14; // convert floating-point sample data back to channel's format
int BASS_ENCODE_BIGEND = 16; // big-endian sample data
int BASS_ENCODE_PAUSE = 32; // start encording paused
int BASS_ENCODE_PCM = 64; // write PCM sample data (no encoder)
int BASS_ENCODE_RF64 = 128; // send an RF64 header
int BASS_ENCODE_QUEUE = 0x200; // queue data to feed encoder asynchronously
int BASS_ENCODE_WFEXT = 0x400; // WAVEFORMATEXTENSIBLE "fmt" chunk
int BASS_ENCODE_CAST_NOLIMIT = 0x1000; // don't limit casting data rate
int BASS_ENCODE_LIMIT = 0x2000; // limit data rate to real-time
int BASS_ENCODE_AIFF = 0x4000; // send an AIFF header rather than WAV
int BASS_ENCODE_DITHER = 0x8000; // apply dither when converting floating-point sample data to integer
int BASS_ENCODE_AUTOFREE = 0x40000; // free the encoder when the channel is freed
// BASS_Encode_GetCount counts
int BASS_ENCODE_COUNT_IN = 0; // sent to encoder
int BASS_ENCODE_COUNT_OUT = 1; // received from encoder
int BASS_ENCODE_COUNT_CAST = 2; // sent to cast server
int BASS_ENCODE_COUNT_QUEUE = 3; // queued
int BASS_ENCODE_COUNT_QUEUE_LIMIT = 4; // queue limit
int BASS_ENCODE_COUNT_QUEUE_FAIL = 5; // failed to queue
int BASS_ENCODE_COUNT_IN_FP = 6; // sent to encoder before floating-point conversion
// BASS_Encode_CastInit content MIME types
String BASS_ENCODE_TYPE_MP3 = "audio/mpeg";
String BASS_ENCODE_TYPE_OGG = "audio/ogg";
String BASS_ENCODE_TYPE_AAC = "audio/aacp";
// BASS_Encode_CastInit flags
int BASS_ENCODE_CAST_PUBLIC = 1; // add to public directory
int BASS_ENCODE_CAST_PUT = 2; // use PUT method
int BASS_ENCODE_CAST_SSL = 4; // use SSL/TLS encryption
// BASS_Encode_CastGetStats types
int BASS_ENCODE_STATS_SHOUT = 0; // Shoutcast stats
int BASS_ENCODE_STATS_ICE = 1; // Icecast mount-point stats
int BASS_ENCODE_STATS_ICESERV = 2; // Icecast server stats
// BASS_Encode_ServerInit flags
int BASS_ENCODE_SERVER_NOHTTP = 1; // no HTTP headers
int BASS_ENCODE_SERVER_META = 2; // Shoutcast metadata
int BASS_ENCODE_SERVER_SSL = 4; // support SSL/TLS encryption
int BASS_ENCODE_SERVER_SSLONLY = 8; // require SSL/TLS encryption
// Encoder notifications
int BASS_ENCODE_NOTIFY_ENCODER = 1; // encoder died
int BASS_ENCODE_NOTIFY_CAST = 2; // cast server connection died
int BASS_ENCODE_NOTIFY_SERVER = 3; // server died
int BASS_ENCODE_NOTIFY_CAST_TIMEOUT = 0x10000; // cast timeout
int BASS_ENCODE_NOTIFY_QUEUE_FULL = 0x10001; // queue is out of space
int BASS_ENCODE_NOTIFY_FREE = 0x10002; // encoder has been freed
interface ENCODEPROC extends Callback {
/**
* Encoding Callback function.
* @param encoderhandle Encoder handle
* @param channelhandle Channel handle
* @param encodedData Buffer containing the encoded data
* @param length number of bytes
* @param user the user pointer passed to BASS_Encode_Start
*/
void ENCODEPROC(int encoderhandle, int channelhandle, Memory encodedData, int length, Pointer user);
}
interface ENCODEPROCEX extends Callback {
/**
* Encoding Callback function
* @param handle Encoder handle
* @param channel Channel handle
* @param buffer Buffer containing the encoded data
* @param length number of bytes
* @param offset file offset of the data
* @param user the user pointer passed to BASS_Encode_Start
*/
void ENCODEPROCEX(int handle, int channel, Memory buffer, int length, long offset, Object user);
}
interface ENCODERPROC extends Callback {
/**
* Encoder Callback function.
* @param encoderHandle Encoder handle
* @param channelHandle Channel handle
* @param encodedData Buffer containing the PCM Data (input) and receiving the encoded data (output)
* @param length Number of bytes in (-1 = closing)
* @param maxOut Maximum number of bytes out
* @param user the user pointer passed to BASS_Encode_Start
* @return the amount of encoded data (-1 = stop)
*/
int ENCODERPROC(int encoderHandle, int channelHandle, Memory encodedData, int length, int maxOut, Pointer user);
}
int BASS_Encode_GetVersion();
int BASS_Encode_Start(int handle, String cmdline, int flags, ENCODEPROC proc, Pointer user);
boolean BASS_Encode_Stop(int handle);
boolean BASS_Encode_Write(int handle, Pointer buffer, int length);
int BASS_Encode_IsActive(int handle);
boolean BASS_Encode_SetPaused(int handle, boolean paused);
int BASS_Encode_GetChannel(int handle);
boolean BASS_Encode_StopEx(int handle, boolean queue);
}

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

@@ -0,0 +1,157 @@
package audio
import audio.Bass.BASS_STREAMPROC_END
import audio.BassEnc.BASS_ENCODE_PCM
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import org.tinylog.Logger
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.net.DatagramPacket
import java.net.DatagramSocket
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
* and writes the received data to a specified file.
* It is designed to run in a separate thread and can be stopped when no longer needed.
* @param listeningAddress The address to listen for incoming UDP packets.
* @param listeningPort The port to listen for incoming UDP packets.
* @param samplingrate The sampling rate for the audio data, default is 44,100 Hz.
* @param channel The number of audio channels, default is 1 (mono).
* @param outputFilePath The path to the file where the received data will be written.
* @param senderIP The IP address of the sender from which to accept packets.
*/
class UDPReceiverToFile(listeningAddress: String, listeningPort: Int, val samplingrate: Int=44100, val channel: Int=1, val outputFilePath: String, val senderIP: String) {
private var socket: DatagramSocket? = null
private val bass : Bass = Bass.Instance
private val bassenc : BassEnc = BassEnc.Instance
private var isReceiving: Boolean = false
private val pipeIn= PipedInputStream(16*1024) // 16K
var isReady: Boolean = false; private set
var bytesReceived: Long = 0; private set
var bytesWritten: Long = 0; private set
init {
try{
val socketaddress = InetSocketAddress(listeningAddress, listeningPort)
socket = DatagramSocket(socketaddress)
isReady = true
} catch (e : Exception) {
Logger.error {"Failed to create UDP socket: ${e.message}" }
}
}
private val streamProc = Bass.STREAMPROC { handle, buffer, length, user ->
try{
val dd = ByteArray(length)
val bytesread = pipeIn.read(dd)
if (bytesread>0){
buffer?.write(0, dd, 0, bytesread) // Write the data to the buffer
bytesWritten += bytesread
// Return the number of bytes read
bytesread
} else {
// if bytesread is 0, it means the pipe is empty, return BASS_STREAMPROC_END
BASS_STREAMPROC_END
}
} catch (e : Exception){
// If an error occurs, log it and return BASS_STREAMPROC_END
Logger.error { "STREAMPROC exception on UDPReceiverToFile $senderIP $outputFilePath" }
BASS_STREAMPROC_END
}
}
/**
* Starts receiving data from the UDP socket and writing it to the specified file.
* This method runs in a separate thread.
* @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message.
* @param udpIsReceiving A Consumer that accepts a Boolean indicating whether UDP is currently receiving
*/
fun startReceiving(callback : BiConsumer<Boolean, String>, udpIsReceiving: Consumer<Boolean>) {
var isReceiving = false
if (isReady){
val scope = CoroutineScope(Dispatchers.Default)
scope.launch(CoroutineName("UDPReceiverToFile UDP $senderIP $outputFilePath")) {
Logger.info { "UDPReceiverToFile started, listening on ${socket?.localSocketAddress} , saving to $outputFilePath" }
PipedOutputStream(pipeIn).use { pipeOut ->
while (isReceiving) {
try{
val xx = DatagramPacket(ByteArray(1500),1500)
socket?.receive(xx)
if (xx.address.hostAddress!= senderIP) continue
if (xx.length < 1) continue
pipeOut.write(xx.data, 0, xx.length)
bytesReceived += xx.length
if (!isReceiving){
isReceiving = true
udpIsReceiving.accept(true)
}
} catch (e : Exception){
Logger.error { "Error receiving UDP packet: ${e.message}" }
continue
}
}
}
Logger.info { "UDPReceiverToFile ended" }
}
scope.launch(CoroutineName("UDPReceiverToFile BASS $senderIP $outputFilePath")) {
bass.BASS_SetDevice(0) // Set to No Sound device, we are not playing audio
val streamhandle = bass.BASS_StreamCreate(samplingrate, channel, 0, streamProc, null)
if (streamhandle!=0){
bass.BASS_ChannelPlay(streamhandle,false)
val encodehandle = bassenc.BASS_Encode_Start(streamhandle, outputFilePath, BASS_ENCODE_PCM, null, null)
if (encodehandle!=0){
Logger.info { "UDPReceiverToFile started writing to $outputFilePath" }
callback.accept(true, "UDPReceiverToFile started successfully, writing to $outputFilePath")
while (isReceiving) {
try {
delay(1000)
} catch (e: InterruptedException) {
Logger.error { "UDPReceiverToFile thread interrupted: ${e.message}" }
break
}
}
bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_StreamFree(streamhandle)
Logger.info { "UDPReceiverToFile stopped writing to $outputFilePath" }
callback.accept(false, "UDPReceiverToFile stopped successfully, written bytes: $bytesWritten")
} else {
callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
}
} else {
callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
}
}
} else callback.accept(false, "UDPReceiverToFile is not ready. Check if the socket was created successfully.")
}
/**
* Stop UDPReceiverToFile from receiving data.
*/
fun stopReceiving() {
isReceiving = false
}
}

View File

@@ -0,0 +1,100 @@
package audio
import audio.Bass.BASS_STREAM_DECODE
import codes.Somecodes.Companion.ValidFile
import com.sun.jna.Memory
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
import java.net.SocketAddress
import java.util.function.BiConsumer
import kotlinx.coroutines.CoroutineName
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
var filehandle: Int = 0
var listSocketAddress = ArrayList<SocketAddress>()
var initialized: Boolean = false; private set
var isRunning: Boolean = false; private set
var bytesSent: Int = 0; private set
init {
if (ValidFile(fileName)){
bass.BASS_SetDevice(0)
val handle = bass.BASS_StreamCreateFile(false, fileName, 0,0, BASS_STREAM_DECODE)
if (handle!=0){
// test buka file berhasil, tutup lagi
bass.BASS_StreamFree(handle)
if (targetPort>0 && targetPort<65535){
if (targetIP.isNotEmpty()){
var validIPs = true
for(ip in targetIP){
try{
var so = InetSocketAddress(ip, targetPort)
listSocketAddress.add(so)
} catch (e : Exception){
validIPs = false
}
}
if (validIPs){
initialized = true
}
}
}
}
}
}
fun Start(callback: BiConsumer<Boolean, String>){
if (initialized){
val scope = CoroutineScope(Dispatchers.Default)
scope.launch(CoroutineName("UDPSenderFromFile $fileName")) {
try {
val socket = DatagramSocket()
bass.BASS_SetDevice(0) // Set to No Sound Device
val handle = bass.BASS_StreamCreateFile(false, fileName, 0, 0, BASS_STREAM_DECODE)
if (handle!=0){
isRunning = true
bytesSent = 0
callback.accept(true,"UDPSenderFromFile started, sending $fileName to ${listSocketAddress.size} targets")
while(isRunning){
val buffer = Memory(bytesPerPackage.toLong())
val bytesRead = bass.BASS_ChannelGetData(handle, buffer, bytesPerPackage)
if (bytesRead > 0) {
for(so in listSocketAddress){
val bytes = buffer.getByteArray(0, bytesRead)
socket.send(DatagramPacket(bytes, bytes.size, so))
bytesSent += bytes.size
}
} else isRunning = false
}
callback.accept(false,"UDPSenderFromFile finished sending $fileName")
bass.BASS_StreamFree(handle)
socket.close()
} else callback.accept(false, "Failed to open file $fileName for reading")
} catch (e : Exception){
callback.accept(false, "Error in UDPSenderFromFile: ${e.message}")
isRunning = false
}
}
} else callback.accept(false, "UDP Sender not initialized, check file and target IP/Port")
}
fun Stop(){
isRunning = false
}
}

View File

@@ -0,0 +1,230 @@
package barix
import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode
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
@Suppress("unused")
class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) {
private var _bR: Int = 0
private var _sd: Int = 0
private var _vu: Int = 0
private var _onlinecounter = 0
private val inet = InetSocketAddress(ipaddress, port)
private val maxUDPsize = 1000
private var _tcp: Socket? = null
/**
* Buffer remain in bytes
*/
var bufferRemain: Int
get() = _bR
set(value) {
_bR = value
_onlinecounter = 5
}
/**
* Status data, 0 = playback idle, 1 = playback running
*/
var statusData: Int
get() = _sd
set(value) {
_sd = if (value < 0) 0 else if (value > 1) 1 else value
_onlinecounter = 5
}
/**
* VU level 0-100
*/
var vu: Int
get() = _vu
set(value) {
_vu = if (value < 0) 0 else if (value > 100) 100 else value
_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
*/
fun decrementOnlineCounter() {
if (_onlinecounter > 0) {
_onlinecounter--
}
}
/**
* Check if Barix device is online
* @return true if online
*/
fun isOnline(): Boolean {
return _onlinecounter > 0
}
/**
* Check if Barix device is idle (not playing)
* @return true if idle
*/
fun isIdle() : Boolean{
return statusData == 0
}
/**
* Check if Barix device is playing
* @return true if playing
*/
fun isPlaying() : Boolean{
return statusData == 1
}
/**
* Send data to Barix device via UDP
* @param data The data to send
*/
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() > 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(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")
}
/**
* Convert BarixConnection to JsonNode
* @return JsonNode representation of BarixConnection
*/
fun toJsonNode(): JsonNode {
// make json node from index, channel, ipaddress, port, bufferRemain, statusData, vu
return Somecodes.objectmapper.createObjectNode().apply {
put("index", index.toInt())
put("channel", channel)
put("ipaddress", ipaddress)
put("port", port)
put("bufferRemain", bufferRemain)
put("statusData", statusData)
put("vu", vu)
put("isOnline", isOnline())
}
}
/**
* Convert BarixConnection to JSON string
* @return JSON string representation of BarixConnection
*/
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
}
}

7
src/barix/BarixStatus.kt Normal file
View File

@@ -0,0 +1,7 @@
package barix
@Suppress("unused")
data class BarixStatus(val ipaddress: String, val vu: Int, val buffremain: Int, val statusdata: Int){
override fun toString(): String {
return "BarixStatus(ipaddress='$ipaddress', vu=$vu, buffremain=$buffremain, statusdata=$statusdata)"
}
}

View File

@@ -0,0 +1,128 @@
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 {
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
private val regex = """STATUSBARIX;(\d+);(\d+);?(\d)?"""
private val pattern = Regex(regex)
/**
* Start TCP Command Server
* @param port The port number to listen on (default is 5001)
* @param cb A callback function that will be called when a valid command is received
* @return true if successful
*/
fun StartTcpServer(port: Int = 5001, cb: Consumer<BarixStatus>): Boolean {
try {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP StreamerOutput server started on port $port" }
while (isActive) {
if (tcpserver.isClosed) break
try {
val socket = tcpserver.accept()
CoroutineScope(Dispatchers.IO).launch {
val key : String = socket.inetAddress.hostAddress
socketMap[key] = socket
Logger.info { "Start communicating with Streamer Output with IP : $key" }
try{
val din = DataInputStream(socket.getInputStream())
while (isActive) {
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 ->
val (vu, buffremain, statusdata) = matchResult.destructured
val status = BarixStatus(
socket.inetAddress.hostAddress,
vu.toInt(),
buffremain.toInt(),
statusdata.toIntOrNull() ?: 0
)
//Logger.info { "Received valid command from $key : $status" }
cb.accept(status)
} ?: run {
Logger.warn { "Invalid command format from $key : $str" }
}
}
}
} catch (ex:Exception){
Logger.error { "Error in communication with Streamer Output with IP $key, Message : ${ex.message}" }
}
Logger.info { "Finished communicating with Streamer Output with IP $key" }
socketMap.remove(key)
}
} catch (ex: Exception) {
Logger.error { "Failed accepting TCP Socket, Message : ${ex.message}" }
}
}
Logger.info { "TCP server stopped" }
}
return true
} catch (e: Exception) {
Logger.error { "Failed to StartTcpServer, Message : ${e.message}" }
}
return false
}
/**
* Stop TCP Command Server
* @return true if succesful
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
}
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]
}
}

14
src/codes/QuadConsumer.kt Normal file
View File

@@ -0,0 +1,14 @@
package codes
@Suppress("unused")
interface QuadConsumer<A,B,C,D> {
/**
* Performs this operation on the given arguments.
*
* @param a the first input argument
* @param b the second input argument
* @param c the third input argument
* @param d the fourth input argument
*/
fun accept(a: A, b: B, c: C, d: D)
}

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

@@ -1,12 +1,530 @@
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
import java.time.format.DateTimeFormatter
import java.util.function.Consumer
import kotlin.io.path.name
@Suppress("unused")
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")
val timeformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm:ss")
val timeformat2: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm")
val filenameformat: DateTimeFormatter = DateTimeFormatter.ofPattern("ddMMyyyy_HHmmss")
const val KB_threshold = 1024.0
const val MB_threshold = KB_threshold * 1024.0
const val GB_threshold = MB_threshold * 1024.0
const val TB_threshold = GB_threshold * 1024.0
val objectmapper = jacksonObjectMapper()
// 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.
*/
fun IsNumber(value: String) : Boolean {
return value.toIntOrNull() != null
}
/**
* Check if a string is alphabetic (contains only letters).
*/
fun IsAlphabethic(value: String) : Boolean {
return value.all { it.isLetter() }
}
/**
* Extract ANN ID from a message string.
* The ANN ID is expected to be a number enclosed in square brackets (e.g., "[123]").
* @param message The message string to extract the ANN ID from.
* @return The extracted ANN ID as an integer, or -1 if not found or invalid.
*/
fun Get_ANN_ID(message: String) : Int {
val matchResult = ann_id_regex.find(message)
return matchResult?.groups?.get(1)?.value?.toInt() ?: -1
}
/**
* Convert an object to a JSON string.
* @param data The object to convert.
* @return A JSON string representation of the object, or "{}" if conversion fails.
*/
fun toJsonString(data: Any) : String {
return try {
objectmapper.writeValueAsString(data)
} catch (e: Exception){
"{}"
}
}
/**
* Convert a JSON string to an object of the specified class.
* @param json The JSON string to convert.
* @param clazz The class of the object to convert to.
* @return An object of the specified class, or null if conversion fails.
*/
fun <T> fromJsonString(json: String, clazz: Class<T>) : T? {
return try {
objectmapper.readValue(json, clazz)
} catch (e: Exception){
null
}
}
/**
* Convert a JSON string to a JsonNode.
* @param data The JSON string to convert.
* @return A JsonNode representation of the JSON string, or empty JsonNode if conversion fails.
*/
fun toJsonNode(data: String) : JsonNode {
return try {
objectmapper.readTree(data)
} catch (e: Exception){
objectmapper.createObjectNode()
}
}
/**
* List all audio files (.mp3 and .wav) in the specified directory and its subdirectories.
* Only files larger than 1KB are included.
* @param p The directory Path to search in
* @return A list of absolute paths to the audio files found.
*/
fun ListAudioFiles(p: Path) : List<String>{
return try{
// find all files that ends with .mp3 or .wav
// and find them recursively
if (Files.exists(p) && Files.isDirectory(p)){
Files.walk(p)
// cari file regular saja
.filter { Files.isRegularFile(it)}
// size lebih dari 1KB
.filter { Files.size(it) > 1024}
// extension .mp3 atau .wav
.filter { it.name.endsWith(".mp3",true) || it.name.endsWith(".wav",true) }
.map { it.toAbsolutePath().toString() }
.toList()
} else throw Exception()
} catch (_ : Exception){
emptyList()
}
}
/**
* Converts a size in bytes to a human-readable format.
* @param size Size in bytes.
* @return A string representing the size in a human-readable format.
*/
fun SizetoHuman(size: Long): String {
return when {
size < KB_threshold -> "${size}B"
size < MB_threshold -> String.format("%.2f KB", size / KB_threshold)
size < GB_threshold -> String.format("%.2f MB", size / MB_threshold)
size < TB_threshold -> String.format("%.2f GB", size / GB_threshold)
else -> String.format("%.2f TB", size / TB_threshold)
}
}
/**
* Get Disk usage using OSHI library.
* @param path The path to check disk usage, defaults to the current working directory.
* @return A string representing the disk usage of the file system in a human-readable format.
*/
fun getDiskUsage(path: String = current_directory) : String {
return try{
val p = Path.of(path).toFile()
if (p.exists() && p.isDirectory){
val total = p.totalSpace
val free = p.freeSpace
val used = total - free
String.format("Total: %s, Used: %s, Free: %s, Usage: %.2f%%",
SizetoHuman(total),
SizetoHuman(used),
SizetoHuman(free),
(used.toDouble() / total * 100)
)
} else throw Exception()
} catch (_ : Exception){
"N/A"
}
}
/**
* Get CPU usage using OSHI library.
* @param cb A callback function that receives the CPU usage as a string.
*/
fun getCPUUsage(cb : Consumer<String>){
CoroutineScope(Dispatchers.Default).launch {
val prev = processor.systemCpuLoadTicks
delay(1000)
val current = processor.systemCpuLoadTicks
fun delta(t: CentralProcessor.TickType) = current[t.index] - prev[t.index]
val idle = delta(CentralProcessor.TickType.IDLE) + delta(CentralProcessor.TickType.IOWAIT)
val busy = delta(CentralProcessor.TickType.USER) + delta(CentralProcessor.TickType.SYSTEM) +
delta(CentralProcessor.TickType.NICE) + delta(CentralProcessor.TickType.IRQ) +
delta(CentralProcessor.TickType.SOFTIRQ)+ delta(CentralProcessor.TickType.STEAL)
val total = idle + busy
val usage = if (total > 0) {
(busy.toDouble() / total) * 100
} else {
0.0
}
cb.accept(String.format("%.2f%%", usage))
}
}
/**
* Get RAM usage using OSHI library.
* @return A string representing the total, used, and available memory in a human-readable format.
*/
fun getMemoryUsage() : String{
val totalMemory = memory.total
val availableMemory = memory.available
val usedMemory = totalMemory - availableMemory
return String.format("Total: %s, Used: %s, Available: %s, Usage: %.2f%%",
SizetoHuman(totalMemory),
SizetoHuman(usedMemory),
SizetoHuman(availableMemory)
, (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.
* @return True if the value is a non-blank string, false otherwise.
*/
fun ValidString(value: Any) : Boolean {
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.
* @return True if the string is a valid file path, false otherwise.
*/
fun ValidFile(value : String) : Boolean {
if (value.isNotBlank()){
return Files.exists(Path.of(value))
}
return false
}
/**
* Check if a string is a valid date in the format "dd/MM/yyyy".
* @param value The string to check.
* @return True if the string is a valid date, false otherwise.
*/
fun ValidDate(value: String): Boolean{
return try{
if (ValidString(value)){
dateformat1.parse(value)
true
} else throw Exception()
} catch (_: Exception){
false
}
}
/**
* Check if a string is a valid IPv4 address.
* @param value The string to check.
* @return True if the string is a valid IPv4 address, false otherwise.
*/
fun ValidIPV4(value: String): Boolean{
return try{
if (ValidString(value)){
val parts = value.split(".")
if (parts.size != 4) return false
for (part in parts){
val num = part.toInt()
if (num !in 0..255) return false
}
true
} else throw Exception()
} catch (_: Exception){
false
}
}
/**
* Check if a string is a valid date in the format "dd-MM-yyyy".
* This format is used for log HTML files.
* @param value The string to check.
* @return True if the string is a valid date, false otherwise.
*/
fun ValiDateForLogHtml(value: String): Boolean{
return try{
if (ValidString(value)){
dateformat2.parse(value)
true
} else throw Exception()
} catch (_: Exception){
false
}
}
/**
* Check if a string is a valid time in the format "hh:mm:ss".
* @param value The string to check.
* @return True if the string is a valid time, false otherwise.
*/
fun ValidTime(value: String): Boolean{
return try{
if (ValidString(value)){
timeformat1.parse(value)
true
} else throw Exception()
} catch (_: Exception){
false
}
}
/**
* Check if a string is a valid schedule time in the format "HH:mm".
* @param value The string to check.
* @return True if the string is a valid schedule time, false otherwise.
*/
fun ValidScheduleTime(value: String): Boolean{
// format HH:mm
try {
if (ValidString(value)){
timeformat2.parse(value)
return true
}
} catch (_ : Exception){
}
return false
}
/**
* Find a schedule day by its name.
* @param value The name of the schedule day to find.
* @return The name of the schedule day if found, null otherwise.
*/
fun FindScheduleDay(value: String) : String? {
val sd = ScheduleDay.entries.find { sd -> sd.name == value }
return sd?.name
}
/**
* Check if a string is a valid schedule day or a valid date.
* A valid schedule day is either one of the ScheduleDay enum names or a date in the format "dd/MM/yyyy".
* @param value The string to check.
* @return True if the string is a valid schedule day or date, false otherwise.
*/
fun ValidScheduleDay(value: String) : Boolean {
if (ValidString(value)){
// check if value is one of ScheduleDay enum name
if (FindScheduleDay(value) != null){
return true
}
// check if value is in format dd/MM/yyyy
return ValidDate(value)
}
return false
}
/**
* Generate a WAV file name with the current date and time.
* The file name format is: [prefix]_ddMMyyyy_HHmmss_[postfix].wav
* @param prefix An optional prefix to add before the date and time.
* @param postfix An optional postfix to add after the date and time.
* @return A string representing the generated WAV file name.
*/
fun Make_WAV_FileName(prefix: String, postfix: String) : String{
val sb = StringBuilder()
if (prefix.isNotEmpty()){sb.append(prefix).append("_")}
sb.append(filenameformat.format(LocalDateTime.now()))
if (postfix.isNotEmpty()){sb.append("_").append(postfix)}
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 ""
}
}

13
src/codes/TriConsumer.kt Normal file
View File

@@ -0,0 +1,13 @@
package codes
@Suppress("unused")
interface TriConsumer<A,B,C> {
/**
* Performs this operation on the given arguments.
*
* @param a the first input argument
* @param b the second input argument
* @param c the third input argument
*/
fun accept(a: A, b: B, c: C)
}

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

@@ -0,0 +1,673 @@
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
import kotlinx.coroutines.Job
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 {
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
* @param port The port to listen on, default is 5003
* @param logCB Callback to handle Log messages
* @return true if successful
*/
fun StartTcpServer(port: Int = 5003, logCB: Consumer<String>): Boolean {
logcb = logCB
try {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP Android server started on port $port" }
while (isActive) {
if (tcpserver.isClosed) break
try {
val socket = tcpserver.accept()
CoroutineScope(Dispatchers.IO).launch {
if (socket != null) {
// key is IP address only
val key: String = socket.inetAddress.hostAddress
socketMap[key] = socket
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) }
.forEach {
process_command(key,it) { reply ->
try {
dout.write(String_to_Byte_Android(reply))
} catch (e: Exception) {
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}")
}
}
logcb.accept("TCP server stopped")
}
return true
} catch (e: Exception) {
logcb.accept("Failed to StartTcpServer, Message : ${e.message}")
}
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
* @return ByteArray with 4 bytes prefix length + string bytes
*/
private fun String_to_Byte_Android(str: String): ByteArray {
if (ValidString(str)) {
val bytes = str.toByteArray(Charsets.UTF_8)
val len = bytes.size
return ByteBuffer.allocate(len + 4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(len)
.put(bytes)
.array()
}
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() }
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}) {
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@")
}
} else {
logcb.accept("Android Login failed from $key with empty username or password")
cb.accept("LOGIN;FALSE@")
}
}
"PCMFILE_START" ->{
// start sending PCM data from Android for paging
val size = parts.getOrElse(1) { "0" }.toInt()
val filename = parts.getOrElse(2) { "" }
val zones = parts.getOrElse(3) { "" }.replace(",",";")
if (size>0){
if (ValidString(filename)){
if (ValidString(zones)){
// create paging job
val pj = PagingJob(key, zones)
// ada expected size
pj.expectedSize = size
// masukin ke list
listOnGoingPaging[key] = pj
Logger.info{"PagingJob created for Android $key, zones: $zones, file: ${pj.filePath.absolutePathString()}"}
tcpreceiver.RequestDataFrom(key) {
// push data ke paging job
pj.addData(it, it.size)
}
cb.accept("PCMFILE_START;OK@")
Logger.info{"Android $key start sending PCM data, expecting $size bytes"}
return
} else logcb.accept("PCMFILE_START from Android $key failed, empty zones")
} else logcb.accept("PCMFILE_START from Android $key failed, empty filename")
} else logcb.accept("PCMFILE_START from Android $key failed, invalid size")
cb.accept("PCMFILE_START;NG@")
}
"PCMFILE_STOP" -> {
// stop sending PCM data from Android for paging
val pj = listOnGoingPaging[key]
if (pj!=null) {
listOnGoingPaging.remove(key)
tcpreceiver.StopRequestDataFrom(key)
// get remaining data
val data = pj.GetData()
pj.Close()
if (data.size==pj.expectedSize){
Logger.info { "Paging job closed from Android $key, total bytes received ${data.size}, writing to file ${pj.filePath.absolutePathString()}" }
val result = audioPlayer.WavWriter(data, pj.filePath.absolutePathString(), true)
if (result.success) {
val qp = QueuePaging(
0u,
LocalDateTime.now().format(datetimeformat1),
"ANDROID",
"PAGING",
pj.filePath.absolutePathString(),
pj.broadcastzones
)
if (db.queuepagingDB.Add(qp)) {
db.queuepagingDB.Resort()
logcb.accept("Paging audio inserted to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
cb.accept("PCMFILE_STOP;OK@")
return
} else logcb.accept("Failed to insert paging audio to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
} else logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : ${result.message}")
} else logcb.accept("PCMFILE_STOP from Android $key received size ${data.size} does not match expected ${pj.expectedSize}")
} else logcb.accept("PCMFILE_STOP from Android $key failed, no ongoing PCM data receiving")
cb.accept("PCMFILE_STOP;NG@")
}
"STARTPAGINGAND" -> {
// Start Paging request from IPM
val zones = parts.getOrElse(1) { "" }.replace(",",";")
if (ValidString(zones)){
// 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" -> {
// 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" -> {
// 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" -> {
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" -> {
// 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 -> {
logcb.accept("Unknown command from Android: $cmd")
}
}
}
/**
* Stop TCP Command Server
* @return true if succesful
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
}
return false
}
}

View File

@@ -0,0 +1,99 @@
package commandServer
import codes.Somecodes.Companion.ValidString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tinylog.Logger
import java.net.ServerSocket
import java.net.Socket
import java.util.function.Consumer
@Suppress("unused")
class TCP_PC_Command_Server {
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
/**
* Start TCP Command Server
* @param port The port number to listen on (default is 5000)
* @param cb A callback function that will be called when a valid command is received
* @return true if successful
*/
fun StartTcpServer(port: Int = 5000, cb: Consumer<String>): Boolean {
try {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
while (isActive) {
if (tcpserver.isClosed) break
try {
tcpserver.accept().use { socket ->
{
CoroutineScope(Dispatchers.IO).launch {
if (socket != null) {
val key : String = socket.inetAddress.hostAddress+":"+socket.port
socketMap[key] = socket
Logger.info { "Start communicating with $key" }
socket.getInputStream().use { din ->
{
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)
if (ValidString(str)) cb.accept(str)
}
}
}
}
Logger.info { "Finished communicating with $key" }
socketMap.remove(key)
}
}
}
}
} catch (ex: Exception) {
Logger.error { "Failed accepting TCP Socket, Message : ${ex.message}" }
}
}
Logger.info { "TCP server stopped" }
}
return true
} catch (e: Exception) {
Logger.error { "Failed to StartTcpServer, Message : ${e.message}" }
}
return false
}
/**
* Stop TCP Command Server
* @return true if succesful
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
}
return false
}
}

View File

@@ -0,0 +1,3 @@
package commandServer
data class userLogin(val ip : String, var username: String)

21
src/content/Category.kt Normal file
View File

@@ -0,0 +1,21 @@
package content
@Suppress("unused")
enum class Category(name: String) {
Airline_Code("Airline_Code"),
Airplane_Name("Airplane_Name"),
AlphabetNumeric("AlphabetNumeric"),
City("City"),
Phrase("Phrase"),
Places("Places"),
PlatNomor("PlatNomor"),
Shalat("Shalat"),
Year("Year"),
Birthday("Birthday"),
Reason("Reason"),
Sequence("Sequence"),
Procedure("Procedure"),
Gate("Gate"),
Greeting("Greeting"),
Compensation("Compensation");
}

16
src/content/Language.kt Normal file
View File

@@ -0,0 +1,16 @@
package content
/**
* Enum class representing different languages.
*
* @property name The name of the language, as in Soundbank Database
*/
@Suppress("unused")
enum class Language(name: String) {
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

@@ -0,0 +1,13 @@
package content
@Suppress("unused")
enum class ScheduleDay(val day: String) {
Sunday("Sunday"),
Monday("Monday"),
Tuesday("Tuesday"),
Wednesday("Wednesday"),
Thursday("Thursday"),
Friday("Friday"),
Saturday("Saturday"),
Everyday("Everyday")
}

View File

@@ -0,0 +1,6 @@
package content
import audio.AudioFileInfo
@Suppress("unused")
data class SoundbankData(val TAG: String, val Category: Category, val Language: Language, val VoiceType: VoiceType, val audio: AudioFileInfo)

13
src/content/VoiceType.kt Normal file
View File

@@ -0,0 +1,13 @@
package content
/**
* Enum class representing different voice types.
*
* @property voicename The name of the voice type.
*/
@Suppress("unused")
enum class VoiceType(voicename: String) {
VOICE_1("VOICE_1"),
VOICE_2("VOICE_2"),
VOICE_3("VOICE_3");
}

View File

@@ -0,0 +1,28 @@
package database
@Suppress("unused")
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')"
}
}

Some files were not shown because too many files have changed in this diff Show More