Compare commits

...

94 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
30c10f863d first commit 26/07/2025 2025-07-26 07:35:24 +07:00
df62bd1cd2 first commit 26/07/2025 2025-07-26 07:27:59 +07:00
131 changed files with 23223 additions and 174 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,13 +1,15 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<Languages>
<language minSize="47" name="Kotlin" />
</Languages>
</inspection_tool>
<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" />
<option name="processLiterals" 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>

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</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.

BIN
libs/linux-armhf/libbass.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/linux-x86/libbass.so Normal file

Binary file not shown.

Binary file not shown.

BIN
libs/win32-arm64/bass.dll Normal file

Binary file not shown.

Binary file not shown.

BIN
libs/win32-x86-64/bass.dll Normal file

Binary file not shown.

Binary file not shown.

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

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
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
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 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)
val db = MariaDB()
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" }
db.close()
}
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()
}
}

309
src/audio/AudioPlayer.kt Normal file
View File

@@ -0,0 +1,309 @@
package audio
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
@Suppress("unused")
class AudioPlayer (var samplingrate: Int) {
val bass: Bass = Bass.Instance
val bassenc : BassEnc = BassEnc.Instance
var initedDevice = -1
init {
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
}
/**
* Initializes the audio system with the specified device ID.
* Call it before using any audio functions.
* @param id The device ID to initialize.
* @return True if initialization was successful, false otherwise.
*/
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" }
if (id > 0) {
initedDevice = id
Logger.info { "Real Audio Device used ID=$initedDevice" }
}
return true
} else {
Logger.error { "Audio ID=$id initialization failed: ${bass.BASS_ErrorGetCode()}" }
return false
}
}
/**
* Uninitializes the audio system if it was previously initialized.
*/
fun Close(){
if (initedDevice != -1) {
bass.BASS_SetDevice(initedDevice)
if (bass.BASS_Free()) {
Logger.info {"Audio ID=$initedDevice uninitialized successfully"}
} else {
Logger.error { "Audio ID=$initedDevice uninitialization failed: ${bass.BASS_ErrorGetCode()}" }
}
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")
}
}
}

792
src/audio/Bass.java Normal file
View File

@@ -0,0 +1,792 @@
package audio;
import com.sun.jna.*;
@SuppressWarnings("unused")
public interface Bass extends Library {
Bass Instance = (Bass) Native.load("bass", Bass.class);
int BASSVERSION = 0x204; // API version
String BASSVERSIONTEXT = "2.4";
// Error codes returned by BASS_ErrorGetCode
int BASS_OK = 0; // all is OK
int BASS_ERROR_MEM = 1; // memory error
int BASS_ERROR_FILEOPEN = 2; // can't open the file
int BASS_ERROR_DRIVER = 3; // can't find a free/valid driver
int BASS_ERROR_BUFLOST = 4; // the sample buffer was lost
int BASS_ERROR_HANDLE = 5; // invalid handle
int BASS_ERROR_FORMAT = 6; // unsupported sample format
int BASS_ERROR_POSITION = 7; // invalid position
int BASS_ERROR_INIT = 8; // BASS_Init has not been successfully called
int BASS_ERROR_START = 9; // BASS_Start has not been successfully called
int BASS_ERROR_SSL = 10; // SSL/HTTPS support isn't available
int BASS_ERROR_REINIT = 11; // device needs to be reinitialized
int BASS_ERROR_ALREADY = 14; // already initialized/paused/whatever
int BASS_ERROR_NOTAUDIO = 17; // file does not contain audio
int BASS_ERROR_NOCHAN = 18; // can't get a free channel
int BASS_ERROR_ILLTYPE = 19; // an illegal type was specified
int BASS_ERROR_ILLPARAM = 20; // an illegal parameter was specified
int BASS_ERROR_NO3D = 21; // no 3D support
int BASS_ERROR_NOEAX = 22; // no EAX support
int BASS_ERROR_DEVICE = 23; // illegal device number
int BASS_ERROR_NOPLAY = 24; // not playing
int BASS_ERROR_FREQ = 25; // illegal sample rate
int BASS_ERROR_NOTFILE = 27; // the stream is not a file stream
int BASS_ERROR_NOHW = 29; // no hardware voices available
int BASS_ERROR_EMPTY = 31; // the file has no sample data
int BASS_ERROR_NONET = 32; // no internet connection could be opened
int BASS_ERROR_CREATE = 33; // couldn't create the file
int BASS_ERROR_NOFX = 34; // effects are not available
int BASS_ERROR_NOTAVAIL = 37; // requested data/action is not available
int BASS_ERROR_DECODE = 38; // the channel is a "decoding channel"
int BASS_ERROR_DX = 39; // a sufficient DirectX version is not installed
int BASS_ERROR_TIMEOUT = 40; // connection timedout
int BASS_ERROR_FILEFORM = 41; // unsupported file format
int BASS_ERROR_SPEAKER = 42; // unavailable speaker
int BASS_ERROR_VERSION = 43; // invalid BASS version (used by add-ons)
int BASS_ERROR_CODEC = 44; // codec is not available/supported
int BASS_ERROR_ENDED = 45; // the channel/file has ended
int BASS_ERROR_BUSY = 46; // the device is busy
int BASS_ERROR_UNSTREAMABLE = 47; // unstreamable file
int BASS_ERROR_PROTOCOL = 48; // unsupported protocol
int BASS_ERROR_DENIED = 49; // access denied
int BASS_ERROR_UNKNOWN = -1; // some other mystery problem
int BASS_ERROR_JAVA_CLASS = 500; // object class problem
// BASS_SetConfig options
int BASS_CONFIG_BUFFER = 0;
int BASS_CONFIG_UPDATEPERIOD = 1;
int BASS_CONFIG_GVOL_SAMPLE = 4;
int BASS_CONFIG_GVOL_STREAM = 5;
int BASS_CONFIG_GVOL_MUSIC = 6;
int BASS_CONFIG_CURVE_VOL = 7;
int BASS_CONFIG_CURVE_PAN = 8;
int BASS_CONFIG_FLOATDSP = 9;
int BASS_CONFIG_3DALGORITHM = 10;
int BASS_CONFIG_NET_TIMEOUT = 11;
int BASS_CONFIG_NET_BUFFER = 12;
int BASS_CONFIG_PAUSE_NOPLAY = 13;
int BASS_CONFIG_NET_PREBUF = 15;
int BASS_CONFIG_NET_PASSIVE = 18;
int BASS_CONFIG_REC_BUFFER = 19;
int BASS_CONFIG_NET_PLAYLIST = 21;
int BASS_CONFIG_MUSIC_VIRTUAL = 22;
int BASS_CONFIG_VERIFY = 23;
int BASS_CONFIG_UPDATETHREADS = 24;
int BASS_CONFIG_DEV_BUFFER = 27;
int BASS_CONFIG_DEV_DEFAULT = 36;
int BASS_CONFIG_NET_READTIMEOUT = 37;
int BASS_CONFIG_HANDLES = 41;
int BASS_CONFIG_SRC = 43;
int BASS_CONFIG_SRC_SAMPLE = 44;
int BASS_CONFIG_ASYNCFILE_BUFFER = 45;
int BASS_CONFIG_OGG_PRESCAN = 47;
int BASS_CONFIG_DEV_NONSTOP = 50;
int BASS_CONFIG_VERIFY_NET = 52;
int BASS_CONFIG_DEV_PERIOD = 53;
int BASS_CONFIG_FLOAT = 54;
int BASS_CONFIG_NET_SEEK = 56;
int BASS_CONFIG_AM_DISABLE = 58;
int BASS_CONFIG_NET_PLAYLIST_DEPTH = 59;
int BASS_CONFIG_NET_PREBUF_WAIT = 60;
int BASS_CONFIG_ANDROID_SESSIONID = 62;
int BASS_CONFIG_ANDROID_AAUDIO = 67;
int BASS_CONFIG_SAMPLE_ONEHANDLE = 69;
int BASS_CONFIG_DEV_TIMEOUT = 70;
int BASS_CONFIG_NET_META = 71;
int BASS_CONFIG_NET_RESTRATE = 72;
int BASS_CONFIG_REC_DEFAULT = 73;
int BASS_CONFIG_NORAMP = 74;
// BASS_SetConfigPtr options
int BASS_CONFIG_NET_AGENT = 16;
int BASS_CONFIG_NET_PROXY = 17;
int BASS_CONFIG_LIBSSL = 64;
int BASS_CONFIG_FILENAME = 75;
int BASS_CONFIG_THREAD = 0x40000000; // flag: thread-specific setting
// BASS_Init flags
int BASS_DEVICE_8BITS = 1; // unused
int BASS_DEVICE_MONO = 2; // mono
int BASS_DEVICE_3D = 4; // unused
int BASS_DEVICE_16BITS = 8; // limit output to 16-bit
int BASS_DEVICE_REINIT = 128; // reinitialize
int BASS_DEVICE_LATENCY = 0x100; // unused
int BASS_DEVICE_SPEAKERS = 0x800; // force enabling of speaker assignment
int BASS_DEVICE_NOSPEAKER = 0x1000; // ignore speaker arrangement
int BASS_DEVICE_DMIX = 0x2000; // use ALSA "dmix" plugin
int BASS_DEVICE_FREQ = 0x4000; // set device sample rate
int BASS_DEVICE_STEREO = 0x8000; // limit output to stereo
int BASS_DEVICE_AUDIOTRACK = 0x20000; // use AudioTrack output
int BASS_DEVICE_DSOUND = 0x40000; // use DirectSound output
int BASS_DEVICE_SOFTWARE = 0x80000; // disable hardware/fastpath output
@Structure.FieldOrder({"name", "driver", "flags"})
class BASS_DEVICEINFO extends Structure {
public String name; // description
public String driver; // driver
public int flags;
}
// BASS_DEVICEINFO flags
int BASS_DEVICE_ENABLED = 1;
int BASS_DEVICE_DEFAULT = 2;
int BASS_DEVICE_INIT = 4;
@Structure.FieldOrder({"flags", "hwsize", "hwfree", "freesam", "free3d", "minrate", "maxrate", "eax", "minbuf", "dsver", "latency", "initflags", "speakers", "freq"})
class BASS_INFO extends Structure{
public int flags; // device capabilities (DSCAPS_xxx flags)
public int hwsize; // unused
public int hwfree; // unused
public int freesam; // unused
public int free3d; // unused
public int minrate; // unused
public int maxrate; // unused
public int eax; // unused
public int minbuf; // recommended minimum buffer length in ms
public int dsver; // DirectSound version
public int latency; // average delay (in ms) before start of playback
public int initflags; // BASS_Init "flags" parameter
public int speakers; // number of speakers available
public int freq; // current output rate
}
// Recording device info structure
@Structure.FieldOrder({"flags", "formats", "inputs", "singlein", "freq"})
class BASS_RECORDINFO extends Structure {
public int flags; // device capabilities (DSCCAPS_xxx flags)
public int formats; // supported standard formats (WAVE_FORMAT_xxx flags)
public int inputs; // number of inputs
public boolean singlein; // TRUE = only 1 input can be set at a time
public int freq; // current input rate
}
// Sample info structure
@Structure.FieldOrder({"freq", "chans", "flags", "length", "max", "origres", "chans", "mingap", "mode3d", "mindist", "maxdist", "iangle", "oangle", "outvol", "vam", "priority"})
class BASS_SAMPLE extends Structure {
public int freq; // default playback rate
public float volume; // default volume (0-1)
public float pan; // default pan (-1=left, 0=middle, 1=right)
public int flags; // BASS_SAMPLE_xxx flags
public int length; // length (in bytes)
public int max; // maximum simultaneous playbacks
public int origres; // original resolution bits
public int chans; // number of channels
public int mingap; // minimum gap (ms) between creating channels
public int mode3d; // BASS_3DMODE_xxx mode
public float mindist; // minimum distance
public float maxdist; // maximum distance
public int iangle; // angle of inside projection cone
public int oangle; // angle of outside projection cone
public float outvol; // delta-volume outside the projection cone
public int vam; // unused
public int priority; // unused
}
int BASS_SAMPLE_8BITS = 1; // 8 bit
int BASS_SAMPLE_FLOAT = 256; // 32-bit floating-point
int BASS_SAMPLE_MONO = 2; // mono
int BASS_SAMPLE_LOOP = 4; // looped
int BASS_SAMPLE_3D = 8; // 3D functionality
int BASS_SAMPLE_SOFTWARE = 16; // unused
int BASS_SAMPLE_MUTEMAX = 32; // mute at max distance (3D only)
int BASS_SAMPLE_VAM = 64; // unused
int BASS_SAMPLE_FX = 128; // unused
int BASS_SAMPLE_OVER_VOL = 0x10000; // override lowest volume
int BASS_SAMPLE_OVER_POS = 0x20000; // override longest playing
int BASS_SAMPLE_OVER_DIST = 0x30000; // override furthest from listener (3D only)
int BASS_STREAM_PRESCAN = 0x20000; // scan file for accurate seeking and length
int BASS_STREAM_AUTOFREE = 0x40000; // automatically free the stream when it stops/ends
int BASS_STREAM_RESTRATE = 0x80000; // restrict the download rate of internet file streams
int BASS_STREAM_BLOCK = 0x100000; // download/play internet file stream in small blocks
int BASS_STREAM_DECODE = 0x200000; // don't play the stream, only decode (BASS_ChannelGetData)
int BASS_STREAM_STATUS = 0x800000; // give server status info (HTTP/ICY tags) in DOWNLOADPROC
int BASS_MP3_IGNOREDELAY = 0x200; // ignore LAME/Xing/VBRI/iTunes delay & padding info
int BASS_MP3_SETPOS = BASS_STREAM_PRESCAN;
int BASS_MUSIC_FLOAT = BASS_SAMPLE_FLOAT;
int BASS_MUSIC_MONO = BASS_SAMPLE_MONO;
int BASS_MUSIC_LOOP = BASS_SAMPLE_LOOP;
int BASS_MUSIC_3D = BASS_SAMPLE_3D;
int BASS_MUSIC_FX = BASS_SAMPLE_FX;
int BASS_MUSIC_AUTOFREE = BASS_STREAM_AUTOFREE;
int BASS_MUSIC_DECODE = BASS_STREAM_DECODE;
int BASS_MUSIC_PRESCAN = BASS_STREAM_PRESCAN; // calculate playback length
int BASS_MUSIC_CALCLEN = BASS_MUSIC_PRESCAN;
int BASS_MUSIC_RAMP = 0x200; // normal ramping
int BASS_MUSIC_RAMPS = 0x400; // sensitive ramping
int BASS_MUSIC_SURROUND = 0x800; // surround sound
int BASS_MUSIC_SURROUND2 = 0x1000; // surround sound (mode 2)
int BASS_MUSIC_FT2PAN = 0x2000; // apply FastTracker 2 panning to XM files
int BASS_MUSIC_FT2MOD = 0x2000; // play .MOD as FastTracker 2 does
int BASS_MUSIC_PT1MOD = 0x4000; // play .MOD as ProTracker 1 does
int BASS_MUSIC_NONINTER = 0x10000; // non-interpolated sample mixing
int BASS_MUSIC_SINCINTER = 0x800000; // sinc interpolated sample mixing
int BASS_MUSIC_POSRESET = 0x8000; // stop all notes when moving position
int BASS_MUSIC_POSRESETEX = 0x400000; // stop all notes and reset bmp/etc when moving position
int BASS_MUSIC_STOPBACK = 0x80000; // stop the music on a backwards jump effect
int BASS_MUSIC_NOSAMPLE = 0x100000; // don't load the samples
// Speaker assignment flags
int BASS_SPEAKER_FRONT = 0x1000000; // front speakers
int BASS_SPEAKER_REAR = 0x2000000; // rear speakers
int BASS_SPEAKER_CENLFE = 0x3000000; // center & LFE speakers (5.1)
int BASS_SPEAKER_SIDE = 0x4000000; // side speakers (7.1)
static int BASS_SPEAKER_N(int n) { return n<<24; } // n'th pair of speakers (max 15)
int BASS_SPEAKER_LEFT = 0x10000000; // modifier: left
int BASS_SPEAKER_RIGHT = 0x20000000; // modifier: right
int BASS_SPEAKER_FRONTLEFT = BASS_SPEAKER_FRONT | BASS_SPEAKER_LEFT;
int BASS_SPEAKER_FRONTRIGHT = BASS_SPEAKER_FRONT | BASS_SPEAKER_RIGHT;
int BASS_SPEAKER_REARLEFT = BASS_SPEAKER_REAR | BASS_SPEAKER_LEFT;
int BASS_SPEAKER_REARRIGHT = BASS_SPEAKER_REAR | BASS_SPEAKER_RIGHT;
int BASS_SPEAKER_CENTER = BASS_SPEAKER_CENLFE | BASS_SPEAKER_LEFT;
int BASS_SPEAKER_LFE = BASS_SPEAKER_CENLFE | BASS_SPEAKER_RIGHT;
int BASS_SPEAKER_SIDELEFT = BASS_SPEAKER_SIDE | BASS_SPEAKER_LEFT;
int BASS_SPEAKER_SIDERIGHT = BASS_SPEAKER_SIDE | BASS_SPEAKER_RIGHT;
int BASS_SPEAKER_REAR2 = BASS_SPEAKER_SIDE;
int BASS_SPEAKER_REAR2LEFT = BASS_SPEAKER_SIDELEFT;
int BASS_SPEAKER_REAR2RIGHT = BASS_SPEAKER_SIDERIGHT;
int BASS_ASYNCFILE = 0x40000000; // read file asynchronously
int BASS_RECORD_PAUSE = 0x8000; // start recording paused
// Channel info structure
@Structure.FieldOrder({"freq", "chans", "flags", "ctype", "origres", "plugin", "sample", "filename"})
class BASS_CHANNELINFO extends Structure {
public int freq; // default playback rate
public int chans; // channels
public int flags;
public int ctype; // type of channel
public int origres; // original resolution
public int plugin;
public int sample;
public String filename;
}
int BASS_ORIGRES_FLOAT = 0x10000;
// BASS_CHANNELINFO types
int BASS_CTYPE_SAMPLE = 1;
int BASS_CTYPE_RECORD = 2;
int BASS_CTYPE_STREAM = 0x10000;
int BASS_CTYPE_STREAM_VORBIS = 0x10002;
int BASS_CTYPE_STREAM_OGG = 0x10002;
int BASS_CTYPE_STREAM_MP1 = 0x10003;
int BASS_CTYPE_STREAM_MP2 = 0x10004;
int BASS_CTYPE_STREAM_MP3 = 0x10005;
int BASS_CTYPE_STREAM_AIFF = 0x10006;
int BASS_CTYPE_STREAM_CA = 0x10007;
int BASS_CTYPE_STREAM_MF = 0x10008;
int BASS_CTYPE_STREAM_AM = 0x10009;
int BASS_CTYPE_STREAM_SAMPLE = 0x1000a;
int BASS_CTYPE_STREAM_DUMMY = 0x18000;
int BASS_CTYPE_STREAM_DEVICE = 0x18001;
int BASS_CTYPE_STREAM_WAV = 0x40000; // WAVE flag (LOWORD=codec)
int BASS_CTYPE_STREAM_WAV_PCM = 0x50001;
int BASS_CTYPE_STREAM_WAV_FLOAT = 0x50003;
int BASS_CTYPE_MUSIC_MOD = 0x20000;
int BASS_CTYPE_MUSIC_MTM = 0x20001;
int BASS_CTYPE_MUSIC_S3M = 0x20002;
int BASS_CTYPE_MUSIC_XM = 0x20003;
int BASS_CTYPE_MUSIC_IT = 0x20004;
int BASS_CTYPE_MUSIC_MO3 = 0x00100; // MO3 flag
@Structure.FieldOrder({"ctype", "name", "exts"})
class BASS_PLUGINFORM extends Structure {
int ctype; // channel type
String name; // format description
String exts; // file extension filter (*.ext1;*.ext2;etc...)
}
@Structure.FieldOrder({"version", "formatc", "formats"})
class BASS_PLUGININFO extends Structure {
int version; // version (same form as BASS_GetVersion)
int formatc; // number of formats
BASS_PLUGINFORM[] formats; // the array of formats
}
// 3D vector (for 3D positions/velocities/orientations)
class BASS_3DVECTOR {
BASS_3DVECTOR() {}
BASS_3DVECTOR(float _x, float _y, float _z) { x=_x; y=_y; z=_z; }
float x; // +=right, -=left
float y; // +=up, -=down
float z; // +=front, -=behind
}
// 3D channel modes
int BASS_3DMODE_NORMAL = 0; // normal 3D processing
int BASS_3DMODE_RELATIVE = 1; // position is relative to the listener
int BASS_3DMODE_OFF = 2; // no 3D processing
// software 3D mixing algorithms (used with BASS_CONFIG_3DALGORITHM)
int BASS_3DALG_DEFAULT = 0;
int BASS_3DALG_OFF = 1;
int BASS_3DALG_FULL = 2;
int BASS_3DALG_LIGHT = 3;
// BASS_SampleGetChannel flags
int BASS_SAMCHAN_NEW = 1; // get a new playback channel
int BASS_SAMCHAN_STREAM = 2; // create a stream
interface STREAMPROC extends Callback
{
int STREAMPROC(int handle, Pointer buffer, int length, Pointer user);
/* User stream callback function.
handle : The stream that needs writing
buffer : Buffer to write the samples in
length : Number of bytes to write
user : The 'user' parameter value given when calling BASS_StreamCreate
RETURN : Number of bytes written. Set the BASS_STREAMPROC_END flag to end
the stream. */
}
int BASS_STREAMPROC_END = 0x80000000; // end of user stream flag
// Special STREAMPROCs
int STREAMPROC_DUMMY = 0; // "dummy" stream
int STREAMPROC_PUSH = -1; // push stream
int STREAMPROC_DEVICE = -2; // device mix stream
int STREAMPROC_DEVICE_3D = -3; // device 3D mix stream
// BASS_StreamCreateFileUser file systems
int STREAMFILE_NOBUFFER = 0;
int STREAMFILE_BUFFER = 1;
int STREAMFILE_BUFFERPUSH = 2;
interface BASS_FILEPROCS extends Callback
{
// User file stream callback functions
void FILECLOSEPROC(Pointer user);
long FILELENPROC(Pointer user) ;
int FILEREADPROC(Pointer buffer, int length, Pointer user);
boolean FILESEEKPROC(long offset, Pointer user);
}
// BASS_StreamPutFileData options
int BASS_FILEDATA_END = 0; // end & close the file
// BASS_StreamGetFilePosition modes
int BASS_FILEPOS_CURRENT = 0;
int BASS_FILEPOS_DECODE = BASS_FILEPOS_CURRENT;
int BASS_FILEPOS_DOWNLOAD = 1;
int BASS_FILEPOS_END = 2;
int BASS_FILEPOS_START = 3;
int BASS_FILEPOS_CONNECTED = 4;
int BASS_FILEPOS_BUFFER = 5;
int BASS_FILEPOS_SOCKET = 6;
int BASS_FILEPOS_ASYNCBUF = 7;
int BASS_FILEPOS_SIZE = 8;
int BASS_FILEPOS_BUFFERING = 9;
int BASS_FILEPOS_AVAILABLE = 10;
interface DOWNLOADPROC extends Callback
{
void DOWNLOADPROC(Pointer buffer, int length, Pointer user);
/* Internet stream download callback function.
buffer : Buffer containing the downloaded data... NULL=end of download
length : Number of bytes in the buffer
user : The 'user' parameter value given when calling BASS_StreamCreateURL */
}
// BASS_ChannelSetSync types
int BASS_SYNC_POS = 0;
int BASS_SYNC_END = 2;
int BASS_SYNC_META = 4;
int BASS_SYNC_SLIDE = 5;
int BASS_SYNC_STALL = 6;
int BASS_SYNC_DOWNLOAD = 7;
int BASS_SYNC_FREE = 8;
int BASS_SYNC_SETPOS = 11;
int BASS_SYNC_MUSICPOS = 10;
int BASS_SYNC_MUSICINST = 1;
int BASS_SYNC_MUSICFX = 3;
int BASS_SYNC_OGG_CHANGE = 12;
int BASS_SYNC_DEV_FAIL = 14;
int BASS_SYNC_DEV_FORMAT = 15;
int BASS_SYNC_THREAD = 0x20000000; // flag: call sync in other thread
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);
/* Sync callback function.
handle : The sync that has occured
channel: Channel that the sync occured in
data : Additional data associated with the sync's occurance
user : The 'user' parameter given when calling BASS_ChannelSetSync */
}
interface DSPPROC extends Callback
{
void DSPPROC(int handle, int channel, Pointer buffer, int length, Pointer user);
/* DSP callback function.
handle : The DSP handle
channel: Channel that the DSP is being applied to
buffer : Buffer to apply the DSP to
length : Number of bytes in the buffer
user : The 'user' parameter given when calling BASS_ChannelSetDSP */
}
interface RECORDPROC extends Callback
{
boolean RECORDPROC(int handle, Pointer buffer, int length, Pointer user);
/* Recording callback function.
handle : The recording handle
buffer : Buffer containing the recorded sample data
length : Number of bytes
user : The 'user' parameter value given when calling BASS_RecordStart
RETURN : true = continue recording, false = stop */
}
// BASS_ChannelIsActive return values
int BASS_ACTIVE_STOPPED = 0;
int BASS_ACTIVE_PLAYING =1;
int BASS_ACTIVE_STALLED = 2;
int BASS_ACTIVE_PAUSED = 3;
int BASS_ACTIVE_PAUSED_DEVICE = 4;
// Channel attributes
int BASS_ATTRIB_FREQ = 1;
int BASS_ATTRIB_VOL = 2;
int BASS_ATTRIB_PAN = 3;
int BASS_ATTRIB_EAXMIX = 4;
int BASS_ATTRIB_NOBUFFER = 5;
int BASS_ATTRIB_VBR = 6;
int BASS_ATTRIB_CPU = 7;
int BASS_ATTRIB_SRC = 8;
int BASS_ATTRIB_NET_RESUME = 9;
int BASS_ATTRIB_SCANINFO = 10;
int BASS_ATTRIB_NORAMP = 11;
int BASS_ATTRIB_BITRATE = 12;
int BASS_ATTRIB_BUFFER = 13;
int BASS_ATTRIB_GRANULE = 14;
int BASS_ATTRIB_USER = 15;
int BASS_ATTRIB_TAIL = 16;
int BASS_ATTRIB_PUSH_LIMIT = 17;
int BASS_ATTRIB_DOWNLOADPROC = 18;
int BASS_ATTRIB_VOLDSP = 19;
int BASS_ATTRIB_VOLDSP_PRIORITY = 20;
int BASS_ATTRIB_MUSIC_AMPLIFY = 0x100;
int BASS_ATTRIB_MUSIC_PANSEP = 0x101;
int BASS_ATTRIB_MUSIC_PSCALER = 0x102;
int BASS_ATTRIB_MUSIC_BPM = 0x103;
int BASS_ATTRIB_MUSIC_SPEED = 0x104;
int BASS_ATTRIB_MUSIC_VOL_GLOBAL = 0x105;
int BASS_ATTRIB_MUSIC_VOL_CHAN = 0x200; // + channel #
int BASS_ATTRIB_MUSIC_VOL_INST = 0x300; // + instrument #
// BASS_ChannelSlideAttribute flags
int BASS_SLIDE_LOG = 0x1000000;
// BASS_ChannelGetData flags
int BASS_DATA_AVAILABLE = 0; // query how much data is buffered
int BASS_DATA_NOREMOVE = 0x10000000; // flag: don't remove data from recording buffer
int BASS_DATA_FIXED = 0x20000000; // unused
int BASS_DATA_FLOAT = 0x40000000; // flag: return floating-point sample data
int BASS_DATA_FFT256 = 0x80000000; // 256 sample FFT
int BASS_DATA_FFT512 = 0x80000001; // 512 FFT
int BASS_DATA_FFT1024 = 0x80000002; // 1024 FFT
int BASS_DATA_FFT2048 = 0x80000003; // 2048 FFT
int BASS_DATA_FFT4096 = 0x80000004; // 4096 FFT
int BASS_DATA_FFT8192 = 0x80000005; // 8192 FFT
int BASS_DATA_FFT16384 = 0x80000006; // 16384 FFT
int BASS_DATA_FFT32768 = 0x80000007; // 32768 FFT
int BASS_DATA_FFT_INDIVIDUAL = 0x10; // FFT flag: FFT for each channel, else all combined
int BASS_DATA_FFT_NOWINDOW = 0x20; // FFT flag: no Hanning window
int BASS_DATA_FFT_REMOVEDC = 0x40; // FFT flag: pre-remove DC bias
int BASS_DATA_FFT_COMPLEX = 0x80; // FFT flag: return complex data
int BASS_DATA_FFT_NYQUIST = 0x100; // FFT flag: return extra Nyquist value
// BASS_ChannelGetLevelEx flags
int BASS_LEVEL_MONO = 1; // get mono level
int BASS_LEVEL_STEREO = 2; // get stereo level
int BASS_LEVEL_RMS = 4; // get RMS levels
int BASS_LEVEL_VOLPAN = 8; // apply VOL/PAN attributes to the levels
int BASS_LEVEL_NOREMOVE = 16; // don't remove data from recording buffer
// BASS_ChannelGetTags types : what's returned
int BASS_TAG_ID3 = 0; // ID3v1 tags : TAG_ID3
int BASS_TAG_ID3V2 = 1; // ID3v2 tags : ByteBuffer
int BASS_TAG_OGG = 2; // OGG comments : String array
int BASS_TAG_HTTP = 3; // HTTP headers : String array
int BASS_TAG_ICY = 4; // ICY headers : String array
int BASS_TAG_META = 5; // ICY metadata : String
int BASS_TAG_APE = 6; // APE tags : String array
int BASS_TAG_MP4 = 7; // MP4/iTunes metadata : String array
int BASS_TAG_VENDOR = 9; // OGG encoder : String
int BASS_TAG_LYRICS3 = 10; // Lyric3v2 tag : String
int BASS_TAG_WAVEFORMAT = 14; // WAVE format : ByteBuffer containing WAVEFORMATEEX structure
int BASS_TAG_AM_NAME = 16; // Android Media codec name : String
int BASS_TAG_ID3V2_2 = 17; // ID3v2 tags (2nd block) : ByteBuffer
int BASS_TAG_AM_MIME = 18; // Android Media MIME type : String
int BASS_TAG_LOCATION = 19; // redirected URL : String
int BASS_TAG_RIFF_INFO = 0x100; // RIFF "INFO" tags : String array
int BASS_TAG_RIFF_BEXT = 0x101; // RIFF/BWF "bext" tags : TAG_BEXT
int BASS_TAG_RIFF_CART = 0x102; // RIFF/BWF "cart" tags : TAG_CART
int BASS_TAG_RIFF_DISP = 0x103; // RIFF "DISP" text tag : String
int BASS_TAG_RIFF_CUE = 0x104; // RIFF "cue " chunk : TAG_CUE structure
int BASS_TAG_RIFF_SMPL = 0x105; // RIFF "smpl" chunk : TAG_SMPL structure
int BASS_TAG_APE_BINARY = 0x1000; // + index #, binary APE tag : TAG_APE_BINARY
int BASS_TAG_MUSIC_NAME = 0x10000; // MOD music name : String
int BASS_TAG_MUSIC_MESSAGE = 0x10001; // MOD message : String
int BASS_TAG_MUSIC_ORDERS = 0x10002; // MOD order list : ByteBuffer
int BASS_TAG_MUSIC_AUTH = 0x10003; // MOD author : UTF-8 string
int BASS_TAG_MUSIC_INST = 0x10100; // + instrument #, MOD instrument name : String
int BASS_TAG_MUSIC_CHAN = 0x10200; // + channel #, MOD channel name : String
int BASS_TAG_MUSIC_SAMPLE = 0x10300; // + sample #, MOD sample name : String
int BASS_TAG_BYTEBUFFER = 0x10000000; // flag: return a ByteBuffer instead of a String or TAG_ID3
// ID3v1 tag structure
@Structure.FieldOrder({"id", "title", "artist", "album", "year", "comment", "genre", "track"})
class TAG_ID3 extends Structure {
String id;
String title;
String artist;
String album;
String year;
String comment;
byte genre;
byte track;
}
// Binary APE tag structure
@Structure.FieldOrder({"key", "data", "length"})
class TAG_APE_BINARY extends Structure {
String key;
Pointer data;
int length;
}
// BASS_ChannelGetLength/GetPosition/SetPosition modes
int BASS_POS_BYTE = 0; // byte position
int BASS_POS_MUSIC_ORDER = 1; // order.row position, MAKELONG(order,row)
int BASS_POS_OGG = 3; // OGG bitstream number
int BASS_POS_END = 0x10; // trimmed end position
int BASS_POS_LOOP = 0x11; // loop start positiom
int BASS_POS_FLUSH = 0x1000000; // flag: flush decoder/FX buffers
int BASS_POS_RESET = 0x2000000; // flag: reset user file buffers
int BASS_POS_RELATIVE = 0x4000000; // flag: seek relative to the current position
int BASS_POS_INEXACT = 0x8000000; // flag: allow seeking to inexact position
int BASS_POS_DECODE = 0x10000000; // flag: get the decoding (not playing) position
int BASS_POS_DECODETO = 0x20000000; // flag: decode to the position instead of seeking
int BASS_POS_SCAN = 0x40000000; // flag: scan to the position
// BASS_ChannelSetDevice/GetDevice option
int BASS_NODEVICE = 0x20000;
// DX8 effect types, use with BASS_ChannelSetFX
int BASS_FX_DX8_CHORUS = 0;
int BASS_FX_DX8_COMPRESSOR = 1;
int BASS_FX_DX8_DISTORTION = 2;
int BASS_FX_DX8_ECHO = 3;
int BASS_FX_DX8_FLANGER = 4;
int BASS_FX_DX8_GARGLE = 5;
int BASS_FX_DX8_I3DL2REVERB = 6;
int BASS_FX_DX8_PARAMEQ = 7;
int BASS_FX_DX8_REVERB = 8;
int BASS_FX_VOLUME = 9;
@Structure.FieldOrder({"fWetDryMix", "fDepth", "fFeedback", "fFrequency", "lWaveform", "fDelay", "lPhase"})
class BASS_DX8_CHORUS extends Structure {
float fWetDryMix;
float fDepth;
float fFeedback;
float fFrequency;
int lWaveform; // 0=triangle, 1=sine
float fDelay;
int lPhase; // BASS_DX8_PHASE_xxx
}
@Structure.FieldOrder({"fGain","fEdge","fPostEQCenterFrequency","fPostEQBandwidth","fPreLowpassCutoff"})
class BASS_DX8_DISTORTION extends Structure {
float fGain;
float fEdge;
float fPostEQCenterFrequency;
float fPostEQBandwidth;
float fPreLowpassCutoff;
}
@Structure.FieldOrder({"fWetDryMix","fFeedback","fLeftDelay","fRightDelay","lPanDelay"})
class BASS_DX8_ECHO extends Structure {
float fWetDryMix;
float fFeedback;
float fLeftDelay;
float fRightDelay;
boolean lPanDelay;
}
@Structure.FieldOrder({"fWetDryMix","fDepth","fFeedback","fFrequency","lWaveform","fDelay","lPhase"})
class BASS_DX8_FLANGER extends Structure {
float fWetDryMix;
float fDepth;
float fFeedback;
float fFrequency;
int lWaveform; // 0=triangle, 1=sine
float fDelay;
int lPhase; // BASS_DX8_PHASE_xxx
}
@Structure.FieldOrder({"fCenter","fBandwidth","fGain"})
class BASS_DX8_PARAMEQ extends Structure {
float fCenter;
float fBandwidth;
float fGain;
}
@Structure.FieldOrder({"fInGain","fReverbMix","fReverbTime","fHighFreqRTRatio"})
class BASS_DX8_REVERB extends Structure {
float fInGain;
float fReverbMix;
float fReverbTime;
float fHighFreqRTRatio;
}
int BASS_DX8_PHASE_NEG_180 = 0;
int BASS_DX8_PHASE_NEG_90 = 1;
int BASS_DX8_PHASE_ZERO = 2;
int BASS_DX8_PHASE_90 = 3;
int BASS_DX8_PHASE_180 = 4;
@Structure.FieldOrder({"fTarget","fCurrent","fTime","lCurve"})
class BASS_FX_VOLUME_PARAM extends Structure {
float fTarget;
float fCurrent;
float fTime;
int lCurve;
}
class FloatValue {
public float value;
}
boolean BASS_SetConfig(int option, int value);
int BASS_GetConfig(int option);
boolean BASS_SetConfigPtr(int option, Pointer value);
Object BASS_GetConfigPtr(int option);
int BASS_GetVersion();
int BASS_ErrorGetCode();
boolean BASS_GetDeviceInfo(int device, BASS_DEVICEINFO info);
boolean BASS_Init(int device, int freq, int flags);
boolean BASS_Free();
boolean BASS_SetDevice(int device);
int BASS_GetDevice();
boolean BASS_GetInfo(BASS_INFO info);
boolean BASS_Start();
boolean BASS_Stop();
boolean BASS_Pause();
int BASS_IsStarted();
boolean BASS_Update(int length);
float BASS_GetCPU();
boolean BASS_SetVolume(float volume);
float BASS_GetVolume();
boolean BASS_Set3DFactors(float distf, float rollf, float doppf);
boolean BASS_Get3DFactors(FloatValue distf, FloatValue rollf, FloatValue doppf);
boolean BASS_Set3DPosition(BASS_3DVECTOR pos, BASS_3DVECTOR vel, BASS_3DVECTOR front, BASS_3DVECTOR top);
boolean BASS_Get3DPosition(BASS_3DVECTOR pos, BASS_3DVECTOR vel, BASS_3DVECTOR front, BASS_3DVECTOR top);
void BASS_Apply3D();
int BASS_PluginLoad(String file, int flags);
boolean BASS_PluginFree(int handle);
boolean BASS_PluginEnable(int handle, boolean enable);
BASS_PLUGININFO BASS_PluginGetInfo(int handle);
int BASS_SampleLoad(String file, long offset, int length, int max, int flags);
int BASS_SampleLoad(Pointer file, long offset, int length, int max, int flags);
int BASS_SampleCreate(int length, int freq, int chans, int max, int flags);
boolean BASS_SampleFree(int handle);
boolean BASS_SampleSetData(int handle, Pointer buffer);
boolean BASS_SampleGetData(int handle, Pointer buffer);
boolean BASS_SampleGetInfo(int handle, BASS_SAMPLE info);
boolean BASS_SampleSetInfo(int handle, BASS_SAMPLE info);
int BASS_SampleGetChannel(int handle, boolean onlynew);
int BASS_SampleGetChannels(int handle, int[] channels);
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);
int BASS_StreamCreateFileUser(int system, int flags, BASS_FILEPROCS procs, Pointer user);
boolean BASS_StreamFree(int handle);
long BASS_StreamGetFilePosition(int handle, int mode);
int BASS_StreamPutData(int handle, Pointer buffer, int length);
int BASS_StreamPutFileData(int handle, Pointer buffer, int length);
int BASS_MusicLoad(String file, long offset, int length, int flags, int freq);
int BASS_MusicLoad(Pointer file, long offset, int length, int flags, int freq);
boolean BASS_MusicFree(int handle);
boolean BASS_RecordGetDeviceInfo(int device, BASS_DEVICEINFO info);
boolean BASS_RecordInit(int device);
boolean BASS_RecordFree();
boolean BASS_RecordSetDevice(int device);
int BASS_RecordGetDevice();
boolean BASS_RecordGetInfo(BASS_RECORDINFO info);
String BASS_RecordGetInputName(int input);
boolean BASS_RecordSetInput(int input, int flags, float volume);
int BASS_RecordGetInput(int input, FloatValue volume);
int BASS_RecordStart(int freq, int chans, int flags, RECORDPROC proc, Pointer user);
double BASS_ChannelBytes2Seconds(int handle, long pos);
long BASS_ChannelSeconds2Bytes(int handle, double pos);
int BASS_ChannelGetDevice(int handle);
boolean BASS_ChannelSetDevice(int handle, int device);
int BASS_ChannelIsActive(int handle);
boolean BASS_ChannelGetInfo(int handle, BASS_CHANNELINFO info);
Object BASS_ChannelGetTags(int handle, int tags);
long BASS_ChannelFlags(int handle, int flags, int mask);
boolean BASS_ChannelLock(int handle, boolean lock);
boolean BASS_ChannelFree(int handle);
boolean BASS_ChannelPlay(int handle, boolean restart);
boolean BASS_ChannelStart(int handle);
boolean BASS_ChannelStop(int handle);
boolean BASS_ChannelPause(int handle);
boolean BASS_ChannelUpdate(int handle, int length);
boolean BASS_ChannelSetAttribute(int handle, int attrib, float value);
boolean BASS_ChannelGetAttribute(int handle, int attrib, FloatValue value);
boolean BASS_ChannelSlideAttribute(int handle, int attrib, float value, int time);
boolean BASS_ChannelIsSliding(int handle, int attrib);
boolean BASS_ChannelSetAttributeEx(int handle, int attrib, Pointer value, int size);
boolean BASS_ChannelSetAttributeDOWNLOADPROC(int handle, DOWNLOADPROC proc, Pointer user);
int BASS_ChannelGetAttributeEx(int handle, int attrib, Pointer value, int size);
boolean BASS_ChannelSet3DAttributes(int handle, int mode, float min, float max, int iangle, int oangle, float outvol);
boolean BASS_ChannelGet3DAttributes(int handle, Integer mode, FloatValue min, FloatValue max, Integer iangle, Integer oangle, FloatValue outvol);
boolean BASS_ChannelSet3DPosition(int handle, BASS_3DVECTOR pos, BASS_3DVECTOR orient, BASS_3DVECTOR vel);
boolean BASS_ChannelGet3DPosition(int handle, BASS_3DVECTOR pos, BASS_3DVECTOR orient, BASS_3DVECTOR vel);
long BASS_ChannelGetLength(int handle, int mode);
boolean BASS_ChannelSetPosition(int handle, long pos, int mode);
long BASS_ChannelGetPosition(int handle, int mode);
int BASS_ChannelGetLevel(int handle);
boolean BASS_ChannelGetLevelEx(int handle, float[] levels, float length, int flags);
int BASS_ChannelGetData(int handle, Pointer buffer, int length);
int BASS_ChannelSetSync(int handle, int type, long param, SYNCPROC proc, Pointer user);
boolean BASS_ChannelRemoveSync(int handle, int sync);
boolean BASS_ChannelSetLink(int handle, int chan);
boolean BASS_ChannelRemoveLink(int handle, int chan);
int BASS_ChannelSetDSP(int handle, DSPPROC proc, Pointer user, int priority);
boolean BASS_ChannelRemoveDSP(int handle, int dsp);
int BASS_ChannelSetFX(int handle, int type, int priority);
boolean BASS_ChannelRemoveFX(int handle, int fx);
boolean BASS_FXSetParameters(int handle, Object params);
boolean BASS_FXGetParameters(int handle, Object params);
boolean BASS_FXSetPriority(int handle, int priority);
boolean BASS_FXReset(int handle);
}

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

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