Compare commits

...

7 Commits

Author SHA1 Message Date
7b4420ddbd commit 13/02/2026 2026-02-13 17:04:20 +07:00
c797c6e7fe commit 12/02/2026 2026-02-12 17:08:20 +07:00
546f2e27af commit 09/02/2026 2026-02-10 17:05:39 +07:00
e18976ace3 commit 09/02/2026 2026-02-09 17:03:50 +07:00
eed96ca8c0 commit 09/02/2026 2026-02-09 12:04:11 +07:00
1790852242 commit 07/02/2026 2026-02-07 17:23:41 +07:00
c8f7f35c79 commit 05/02/2026 2026-02-06 17:02:23 +07:00
89 changed files with 98696 additions and 4727 deletions

10
.idea/libraries/batoulapps_adhan.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<component name="libraryTable">
<library name="batoulapps.adhan" type="repository">
<properties maven-id="com.batoulapps.adhan:adhan:1.2.1" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/batoulapps/adhan/adhan/1.2.1/adhan-1.2.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -0,0 +1,16 @@
<component name="libraryTable">
<library name="batoulapps.adhan.adhan2" type="repository">
<properties maven-id="com.batoulapps.adhan:adhan2:0.0.6" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/batoulapps/adhan/adhan2/0.0.6/adhan2-0.0.6.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-datetime/0.7.1/kotlinx-datetime-0.7.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-serialization-core/1.6.2/kotlinx-serialization-core-1.6.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-serialization-core-jvm/1.6.2/kotlinx-serialization-core-jvm-1.6.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.21/kotlin-stdlib-common-1.9.21.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.2.20/kotlin-stdlib-2.2.20.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -0,0 +1,15 @@
<component name="libraryTable">
<library name="jetbrains.kotlinx.datetime" type="repository">
<properties maven-id="org.jetbrains.kotlinx:kotlinx-datetime:0.7.0-0.6.x-compat" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-datetime/0.7.0-0.6.x-compat/kotlinx-datetime-0.7.0-0.6.x-compat.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.1.20/kotlin-stdlib-2.1.20.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-serialization-core/1.6.2/kotlinx-serialization-core-1.6.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-serialization-core-jvm/1.6.2/kotlinx-serialization-core-jvm-1.6.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.21/kotlin-stdlib-common-1.9.21.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -10,6 +10,7 @@
<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" />
<sourceFolder url="file://$MODULE_DIR$/OurAirports" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
@@ -38,5 +39,6 @@
</orderEntry>
<orderEntry type="library" name="google.cloud.texttospeech" level="project" />
<orderEntry type="library" name="projectlombok.lombok" level="project" />
<orderEntry type="library" name="batoulapps.adhan" level="project" />
</component>
</module>

84563
OurAirports/world-airports.csv Normal file

File diff suppressed because it is too large Load Diff

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,253 @@
/* !
* ClockPicker v0.2.2 for Bootstrap (https://weareoutman.github.io/clockpicker/)
* Copyright 2014 Wang Shenwei
* Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE)
* Bootstrap 4 compatibility by djibe (https://github.com/djibe/clockpicker) */
:root {
--primary-color: 0, 123, 255;
}
.clockpicker .input-group-addon {
cursor: pointer;
}
.clockpicker-moving {
cursor: move;
}
.clockpicker-align-left.popover > .arrow {
left: 25px;
}
.clockpicker-align-top.popover > .arrow {
top: 17px;
}
.clockpicker-align-right.popover > .arrow {
left: auto;
right: 25px;
}
.clockpicker-align-bottom.popover > .arrow {
top: auto;
bottom: 6px;
}
.clockpicker-popover {
-webkit-animation: pickerFadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
animation: pickerFadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
border: 0;
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center top 0px;
transform-origin: center top 0px;
-webkit-box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), 0 11px 15px 0 rgba(0, 0, 0, 0.2);
box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), 0 11px 15px 0 rgba(0, 0, 0, 0.2);
}
.clockpicker-popover.top {
-webkit-transform-origin: center bottom 0px;
transform-origin: center bottom 0px;
}
.clockpicker-popover * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.clockpicker-popover .popover-header {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
background-color: var(--primary, #007bff);
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 3rem;
font-weight: normal;
letter-spacing: normal;
text-align: center;
padding: 0.5rem;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.clockpicker-popover .popover-header span {
cursor: pointer;
}
.clockpicker-popover .popover-body {
background-color: #fff;
padding: 1rem 0.75rem 0.75rem;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.clockpicker-popover .btn {
border: 0 !important;
border-radius: 4px;
-webkit-box-shadow: none;
box-shadow: none;
font-size: 0.8125rem;
font-weight: 500;
padding: 0.59375rem 1rem;
min-width: 0;
margin: 0;
margin-left: 0.25rem;
text-transform: uppercase;
}
.clockpicker-popover .btn:focus, .clockpicker-popover .btn:hover, .clockpicker-popover .btn:active {
outline: none !important;
background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.12)), to(rgba(0, 0, 0, 0.12)));
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12));
-webkit-box-shadow: none;
box-shadow: none;
}
.clockpicker-span-hours {
margin-right: 0.25rem;
}
.clockpicker-span-minutes {
margin-left: 0.25rem;
}
.clockpicker-close-block {
margin-top: 0.75rem;
}
.clockpicker-plate {
background-color: #ededee;
border-radius: 50%;
width: 200px;
height: 200px;
overflow: visible;
position: relative;
}
.clockpicker-canvas, .clockpicker-dial {
width: 200px;
height: 200px;
position: absolute;
left: -1px;
top: -1px;
}
.clockpicker-minutes {
visibility: hidden;
}
.clockpicker-tick {
border-radius: 50%;
line-height: 26px;
text-align: center;
width: 26px;
height: 26px;
position: absolute;
cursor: pointer;
}
.clockpicker-tick.active, .clockpicker-tick:not(.disabled):hover {
background-color: rgba(var(--primary-color, 0, 123, 255), 0.25);
}
.clockpicker-tick.disabled {
color: #eee;
cursor: default;
}
.clockpicker-dial {
-webkit-transition: opacity 350ms, -webkit-transform 350ms;
transition: opacity 350ms, -webkit-transform 350ms;
transition: transform 350ms, opacity 350ms;
transition: transform 350ms, opacity 350ms, -webkit-transform 350ms;
}
.clockpicker-dial-out {
opacity: 0;
}
.clockpicker-hours.clockpicker-dial-out {
-webkit-transform: scale(1.2, 1.2);
transform: scale(1.2, 1.2);
}
.clockpicker-minutes.clockpicker-dial-out {
-webkit-transform: scale(0.8, 0.8);
transform: scale(0.8, 0.8);
}
.clockpicker-canvas {
-webkit-transition: opacity 175ms;
transition: opacity 175ms;
}
.clockpicker-canvas-out {
opacity: 0.25;
}
.clockpicker-canvas line {
stroke: var(--primary, #007bff);
stroke-width: 2;
stroke-linecap: round;
}
.clockpicker-canvas-bearing {
stroke: none;
fill: var(--primary, #007bff);
}
.clockpicker-canvas-fg {
stroke: none;
fill: rgba(var(--primary-color, 0, 123, 255), 0.5);
}
.clockpicker-canvas-bg {
stroke: none;
fill: rgba(var(--primary-color, 0, 123, 255), 0.25);
}
.clockpicker-canvas-bg-trans {
fill: rgba(var(--primary-color, 0, 123, 255), 0.25);
}
.clockpicker-buttons-am-pm {
color: white;
display: none;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-pack: distribute;
justify-content: space-around;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 1rem;
margin-left: 0.75rem;
}
@keyframes pickerFadeIn {
from {
opacity: 0;
-webkit-transform: scale(0.8);
transform: scale(0.8);
}
to {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@@ -0,0 +1,252 @@
/* !
* ClockPicker v0.2.2 for Bootstrap (https://weareoutman.github.io/clockpicker/)
* Copyright 2014 Wang Shenwei
* Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE)
* Bootstrap 4 compatibility by djibe (https://github.com/djibe/clockpicker) */
:root {
--primary-color: 0,123,255;
}
.clockpicker .input-group-addon {
cursor: pointer;
}
.clockpicker-moving {
cursor: move;
}
.clockpicker-align-left.popover > .arrow {
left: 25px;
}
.clockpicker-align-top.popover > .arrow {
top: 17px;
}
.clockpicker-align-right.popover > .arrow {
left: auto;
right: 25px;
}
.clockpicker-align-bottom.popover > .arrow {
top: auto;
bottom: 6px;
}
.clockpicker-popover {
-webkit-animation: pickerFadeIn .2s cubic-bezier(.4,0,.2,1);
animation: pickerFadeIn .2s cubic-bezier(.4,0,.2,1);
border-radius: 4px;
border: 0;
-webkit-transform: scale(1);
transform: scale(1);
-webkit-transform-origin: center top 0;
transform-origin: center top 0;
-webkit-box-shadow: 0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12),0 11px 15px 0 rgba(0,0,0,.2);
box-shadow: 0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12),0 11px 15px 0 rgba(0,0,0,.2);
}
.clockpicker-popover.top {
-webkit-transform-origin: center bottom 0;
transform-origin: center bottom 0;
}
.clockpicker-popover * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.clockpicker-popover .popover-header {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
background-color: var(--primary,#007bff);
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 3rem;
font-weight: 400;
letter-spacing: normal;
text-align: center;
padding: .5rem;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.clockpicker-popover .popover-header span {
cursor: pointer;
}
.clockpicker-popover .popover-body {
background-color: #fff;
padding: 1rem .75rem .75rem;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.clockpicker-popover .btn {
border: 0!important;
border-radius: 4px;
-webkit-box-shadow: none;
box-shadow: none;
font-size: .8125rem;
font-weight: 500;
padding: .59375rem 1rem;
min-width: 0;
margin: 0 0 0 .25rem;
text-transform: uppercase;
}
.clockpicker-popover .btn:active, .clockpicker-popover .btn:focus, .clockpicker-popover .btn:hover {
outline: 0!important;
background-image: -webkit-gradient(linear,left top,left bottom,from(rgba(0,0,0,.12)),to(rgba(0,0,0,.12)));
background-image: linear-gradient(180deg,rgba(0,0,0,.12),rgba(0,0,0,.12));
-webkit-box-shadow: none;
box-shadow: none;
}
.clockpicker-span-hours {
margin-right: .25rem;
}
.clockpicker-span-minutes {
margin-left: .25rem;
}
.clockpicker-close-block {
margin-top: .75rem;
}
.clockpicker-plate {
background-color: #ededee;
border-radius: 50%;
width: 200px;
height: 200px;
overflow: visible;
position: relative;
}
.clockpicker-canvas, .clockpicker-dial {
width: 200px;
height: 200px;
position: absolute;
left: -1px;
top: -1px;
}
.clockpicker-minutes {
visibility: hidden;
}
.clockpicker-tick {
border-radius: 50%;
line-height: 26px;
text-align: center;
width: 26px;
height: 26px;
position: absolute;
cursor: pointer;
}
.clockpicker-tick.active, .clockpicker-tick:not(.disabled):hover {
background-color: rgba(var(--primary-color,0,123,255),.25);
}
.clockpicker-tick.disabled {
color: #eee;
cursor: default;
}
.clockpicker-dial {
-webkit-transition: opacity 350ms,-webkit-transform 350ms;
transition: opacity 350ms,-webkit-transform 350ms;
transition: transform 350ms,opacity 350ms;
transition: transform 350ms,opacity 350ms,-webkit-transform 350ms;
}
.clockpicker-dial-out {
opacity: 0;
}
.clockpicker-hours.clockpicker-dial-out {
-webkit-transform: scale(1.2,1.2);
transform: scale(1.2,1.2);
}
.clockpicker-minutes.clockpicker-dial-out {
-webkit-transform: scale(.8,.8);
transform: scale(.8,.8);
}
.clockpicker-canvas {
-webkit-transition: opacity 175ms;
transition: opacity 175ms;
}
.clockpicker-canvas-out {
opacity: .25;
}
.clockpicker-canvas line {
stroke: var(--primary,#007bff);
stroke-width: 2;
stroke-linecap: round;
}
.clockpicker-canvas-bearing {
stroke: none;
fill: var(--primary,#007bff);
}
.clockpicker-canvas-fg {
stroke: none;
fill: rgba(var(--primary-color,0,123,255),.5);
}
.clockpicker-canvas-bg {
stroke: none;
fill: rgba(var(--primary-color,0,123,255),.25);
}
.clockpicker-canvas-bg-trans {
fill: rgba(var(--primary-color,0,123,255),.25);
}
.clockpicker-buttons-am-pm {
color: #fff;
display: none;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-pack: distribute;
justify-content: space-around;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 1rem;
margin-left: .75rem;
}
@keyframes pickerFadeIn {
from {
opacity: 0;
-webkit-transform: scale(.8);
transform: scale(.8);
}
to {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@@ -58,10 +58,6 @@
margin-right: .5rem!important;
}
.mb-2 {
margin-bottom: .5rem!important;
}
.mb-3 {
margin-bottom: 1rem!important;
}

View File

@@ -0,0 +1,806 @@
.flatpickr-calendar {
background: transparent;
opacity: 0;
display: none;
text-align: center;
visibility: hidden;
padding: 0;
-webkit-animation: none;
animation: none;
direction: ltr;
border: 0;
font-size: 14px;
line-height: 24px;
border-radius: 5px;
position: absolute;
width: 307.875px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-ms-touch-action: manipulation;
touch-action: manipulation;
background: #fff;
-webkit-box-shadow: 1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);
box-shadow: 1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);
}
.flatpickr-calendar.open, .flatpickr-calendar.inline {
opacity: 1;
max-height: 640px;
visibility: visible;
}
.flatpickr-calendar.open {
display: inline-block;
z-index: 99999;
}
.flatpickr-calendar.animate.open {
-webkit-animation: fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);
animation: fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);
}
.flatpickr-calendar.inline {
display: block;
position: relative;
top: 2px;
}
.flatpickr-calendar.static {
position: absolute;
top: calc(100% + 2px);
}
.flatpickr-calendar.static.open {
z-index: 999;
display: block;
}
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n + 1) .flatpickr-day.inRange:nth-child(7n + 7) {
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n + 2) .flatpickr-day.inRange:nth-child(7n + 1) {
-webkit-box-shadow: -2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;
box-shadow: -2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;
}
.flatpickr-calendar .hasWeeks .dayContainer, .flatpickr-calendar .hasTime .dayContainer {
border-bottom: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.flatpickr-calendar .hasWeeks .dayContainer {
border-left: 0;
}
.flatpickr-calendar.hasTime .flatpickr-time {
height: 40px;
border-top: 1px solid #e6e6e6;
}
.flatpickr-calendar.noCalendar.hasTime .flatpickr-time {
height: auto;
}
.flatpickr-calendar:before, .flatpickr-calendar:after {
position: absolute;
display: block;
pointer-events: none;
border: solid transparent;
content: '';
height: 0;
width: 0;
left: 22px;
}
.flatpickr-calendar.rightMost:before, .flatpickr-calendar.arrowRight:before, .flatpickr-calendar.rightMost:after, .flatpickr-calendar.arrowRight:after {
left: auto;
right: 22px;
}
.flatpickr-calendar.arrowCenter:before, .flatpickr-calendar.arrowCenter:after {
left: 50%;
right: 50%;
}
.flatpickr-calendar:before {
border-width: 5px;
margin: 0 -5px;
}
.flatpickr-calendar:after {
border-width: 4px;
margin: 0 -4px;
}
.flatpickr-calendar.arrowTop:before, .flatpickr-calendar.arrowTop:after {
bottom: 100%;
}
.flatpickr-calendar.arrowTop:before {
border-bottom-color: #e6e6e6;
}
.flatpickr-calendar.arrowTop:after {
border-bottom-color: #fff;
}
.flatpickr-calendar.arrowBottom:before, .flatpickr-calendar.arrowBottom:after {
top: 100%;
}
.flatpickr-calendar.arrowBottom:before {
border-top-color: #e6e6e6;
}
.flatpickr-calendar.arrowBottom:after {
border-top-color: #fff;
}
.flatpickr-calendar:focus {
outline: 0;
}
.flatpickr-wrapper {
position: relative;
display: inline-block;
}
.flatpickr-months {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.flatpickr-months .flatpickr-month {
background: transparent;
color: rgba(0,0,0,0.9);
fill: rgba(0,0,0,0.9);
height: 34px;
line-height: 1;
text-align: center;
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
overflow: hidden;
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.flatpickr-months .flatpickr-prev-month, .flatpickr-months .flatpickr-next-month {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-decoration: none;
cursor: pointer;
position: absolute;
top: 0;
height: 34px;
padding: 10px;
z-index: 3;
color: rgba(0,0,0,0.9);
fill: rgba(0,0,0,0.9);
}
.flatpickr-months .flatpickr-prev-month.flatpickr-disabled, .flatpickr-months .flatpickr-next-month.flatpickr-disabled {
display: none;
}
.flatpickr-months .flatpickr-prev-month i, .flatpickr-months .flatpickr-next-month i {
position: relative;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month, .flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
left: 0;
}
/* /*rtl:begin:ignore */
/* /*rtl:end:ignore */
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month, .flatpickr-months .flatpickr-next-month.flatpickr-next-month {
right: 0;
}
/* /*rtl:begin:ignore */
/* /*rtl:end:ignore */
.flatpickr-months .flatpickr-prev-month:hover, .flatpickr-months .flatpickr-next-month:hover {
color: #959ea9;
}
.flatpickr-months .flatpickr-prev-month:hover svg, .flatpickr-months .flatpickr-next-month:hover svg {
fill: #f64747;
}
.flatpickr-months .flatpickr-prev-month svg, .flatpickr-months .flatpickr-next-month svg {
width: 14px;
height: 14px;
}
.flatpickr-months .flatpickr-prev-month svg path, .flatpickr-months .flatpickr-next-month svg path {
-webkit-transition: fill .1s;
transition: fill .1s;
fill: inherit;
}
.numInputWrapper {
position: relative;
height: auto;
}
.numInputWrapper input, .numInputWrapper span {
display: inline-block;
}
.numInputWrapper input {
width: 100%;
}
.numInputWrapper input::-ms-clear {
display: none;
}
.numInputWrapper input::-webkit-outer-spin-button, .numInputWrapper input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
}
.numInputWrapper span {
position: absolute;
right: 0;
width: 14px;
padding: 0 4px 0 2px;
height: 50%;
line-height: 50%;
opacity: 0;
cursor: pointer;
border: 1px solid rgba(57,57,57,0.15);
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.numInputWrapper span:hover {
background: rgba(0,0,0,0.1);
}
.numInputWrapper span:active {
background: rgba(0,0,0,0.2);
}
.numInputWrapper span:after {
display: block;
content: "";
position: absolute;
}
.numInputWrapper span.arrowUp {
top: 0;
border-bottom: 0;
}
.numInputWrapper span.arrowUp:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid rgba(57,57,57,0.6);
top: 26%;
}
.numInputWrapper span.arrowDown {
top: 50%;
}
.numInputWrapper span.arrowDown:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid rgba(57,57,57,0.6);
top: 40%;
}
.numInputWrapper span svg {
width: inherit;
height: auto;
}
.numInputWrapper span svg path {
fill: rgba(0,0,0,0.5);
}
.numInputWrapper:hover {
background: rgba(0,0,0,0.05);
}
.numInputWrapper:hover span {
opacity: 1;
}
.flatpickr-current-month {
font-size: 135%;
line-height: inherit;
font-weight: 300;
color: inherit;
position: absolute;
width: 75%;
left: 12.5%;
padding: 7.48px 0 0 0;
line-height: 1;
height: 34px;
display: inline-block;
text-align: center;
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
.flatpickr-current-month span.cur-month {
font-family: inherit;
font-weight: 700;
color: inherit;
display: inline-block;
margin-left: .5ch;
padding: 0;
}
.flatpickr-current-month span.cur-month:hover {
background: rgba(0,0,0,0.05);
}
.flatpickr-current-month .numInputWrapper {
width: 6ch;
width: 7ch\0;
display: inline-block;
}
.flatpickr-current-month .numInputWrapper span.arrowUp:after {
border-bottom-color: rgba(0,0,0,0.9);
}
.flatpickr-current-month .numInputWrapper span.arrowDown:after {
border-top-color: rgba(0,0,0,0.9);
}
.flatpickr-current-month input.cur-year {
background: transparent;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: inherit;
cursor: text;
padding: 0 0 0 .5ch;
margin: 0;
display: inline-block;
font-size: inherit;
font-family: inherit;
font-weight: 300;
line-height: inherit;
height: auto;
border: 0;
border-radius: 0;
vertical-align: initial;
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
}
.flatpickr-current-month input.cur-year:focus {
outline: 0;
}
.flatpickr-current-month input.cur-year[disabled], .flatpickr-current-month input.cur-year[disabled]:hover {
font-size: 100%;
color: rgba(0,0,0,0.5);
background: transparent;
pointer-events: none;
}
.flatpickr-current-month .flatpickr-monthDropdown-months {
appearance: menulist;
background: transparent;
border: none;
border-radius: 0;
box-sizing: border-box;
color: inherit;
cursor: pointer;
font-size: inherit;
font-family: inherit;
font-weight: 300;
height: auto;
line-height: inherit;
margin: -1px 0 0 0;
outline: none;
padding: 0 0 0 .5ch;
position: relative;
vertical-align: initial;
-webkit-box-sizing: border-box;
-webkit-appearance: menulist;
-moz-appearance: menulist;
width: auto;
}
.flatpickr-current-month .flatpickr-monthDropdown-months:focus, .flatpickr-current-month .flatpickr-monthDropdown-months:active {
outline: none;
}
.flatpickr-current-month .flatpickr-monthDropdown-months:hover {
background: rgba(0,0,0,0.05);
}
.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month {
background-color: transparent;
outline: none;
padding: 0;
}
.flatpickr-weekdays {
background: transparent;
text-align: center;
overflow: hidden;
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
height: 28px;
}
.flatpickr-weekdays .flatpickr-weekdaycontainer {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
span.flatpickr-weekday {
cursor: default;
font-size: 90%;
background: transparent;
color: rgba(0,0,0,0.54);
line-height: 1;
margin: 0;
text-align: center;
display: block;
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
font-weight: bolder;
}
.dayContainer, .flatpickr-weeks {
padding: 1px 0 0 0;
}
.flatpickr-days {
position: relative;
overflow: hidden;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: start;
-webkit-align-items: flex-start;
-ms-flex-align: start;
align-items: flex-start;
width: 307.875px;
}
.flatpickr-days:focus {
outline: 0;
}
.dayContainer {
padding: 0;
outline: 0;
text-align: left;
width: 307.875px;
min-width: 307.875px;
max-width: 307.875px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
display: inline-block;
display: -ms-flexbox;
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
-ms-flex-wrap: wrap;
-ms-flex-pack: justify;
-webkit-justify-content: space-around;
justify-content: space-around;
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
opacity: 1;
}
.dayContainer + .dayContainer {
-webkit-box-shadow: -1px 0 0 #e6e6e6;
box-shadow: -1px 0 0 #e6e6e6;
}
.flatpickr-day {
background: none;
border: 1px solid transparent;
border-radius: 150px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #393939;
cursor: pointer;
font-weight: 400;
width: 14.2857143%;
-webkit-flex-basis: 14.2857143%;
-ms-flex-preferred-size: 14.2857143%;
flex-basis: 14.2857143%;
max-width: 39px;
height: 39px;
line-height: 39px;
margin: 0;
display: inline-block;
position: relative;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.flatpickr-day.inRange, .flatpickr-day.prevMonthDay.inRange, .flatpickr-day.nextMonthDay.inRange, .flatpickr-day.today.inRange, .flatpickr-day.prevMonthDay.today.inRange, .flatpickr-day.nextMonthDay.today.inRange, .flatpickr-day:hover, .flatpickr-day.prevMonthDay:hover, .flatpickr-day.nextMonthDay:hover, .flatpickr-day:focus, .flatpickr-day.prevMonthDay:focus, .flatpickr-day.nextMonthDay:focus {
cursor: pointer;
outline: 0;
background: #e6e6e6;
border-color: #e6e6e6;
}
.flatpickr-day.today {
border-color: #959ea9;
}
.flatpickr-day.today:hover, .flatpickr-day.today:focus {
border-color: #959ea9;
background: #959ea9;
color: #fff;
}
.flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange, .flatpickr-day.selected.inRange, .flatpickr-day.startRange.inRange, .flatpickr-day.endRange.inRange, .flatpickr-day.selected:focus, .flatpickr-day.startRange:focus, .flatpickr-day.endRange:focus, .flatpickr-day.selected:hover, .flatpickr-day.startRange:hover, .flatpickr-day.endRange:hover, .flatpickr-day.selected.prevMonthDay, .flatpickr-day.startRange.prevMonthDay, .flatpickr-day.endRange.prevMonthDay, .flatpickr-day.selected.nextMonthDay, .flatpickr-day.startRange.nextMonthDay, .flatpickr-day.endRange.nextMonthDay {
background: #569ff7;
-webkit-box-shadow: none;
box-shadow: none;
color: #fff;
border-color: #569ff7;
}
.flatpickr-day.selected.startRange, .flatpickr-day.startRange.startRange, .flatpickr-day.endRange.startRange {
border-radius: 50px 0 0 50px;
}
.flatpickr-day.selected.endRange, .flatpickr-day.startRange.endRange, .flatpickr-day.endRange.endRange {
border-radius: 0 50px 50px 0;
}
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)), .flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)), .flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
-webkit-box-shadow: -10px 0 0 #569ff7;
box-shadow: -10px 0 0 #569ff7;
}
.flatpickr-day.selected.startRange.endRange, .flatpickr-day.startRange.startRange.endRange, .flatpickr-day.endRange.startRange.endRange {
border-radius: 50px;
}
.flatpickr-day.inRange {
border-radius: 0;
-webkit-box-shadow: -5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;
box-shadow: -5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;
}
.flatpickr-day.flatpickr-disabled, .flatpickr-day.flatpickr-disabled:hover, .flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay, .flatpickr-day.notAllowed, .flatpickr-day.notAllowed.prevMonthDay, .flatpickr-day.notAllowed.nextMonthDay {
color: rgba(57,57,57,0.3);
background: transparent;
border-color: transparent;
cursor: default;
}
.flatpickr-day.flatpickr-disabled, .flatpickr-day.flatpickr-disabled:hover {
cursor: not-allowed;
color: rgba(57,57,57,0.1);
}
.flatpickr-day.week.selected {
border-radius: 0;
-webkit-box-shadow: -5px 0 0 #569ff7,5px 0 0 #569ff7;
box-shadow: -5px 0 0 #569ff7,5px 0 0 #569ff7;
}
.flatpickr-day.hidden {
visibility: hidden;
}
.rangeMode .flatpickr-day {
margin-top: 1px;
}
.flatpickr-weekwrapper {
float: left;
}
.flatpickr-weekwrapper .flatpickr-weeks {
padding: 0 12px;
-webkit-box-shadow: 1px 0 0 #e6e6e6;
box-shadow: 1px 0 0 #e6e6e6;
}
.flatpickr-weekwrapper .flatpickr-weekday {
float: none;
width: 100%;
line-height: 28px;
}
.flatpickr-weekwrapper span.flatpickr-day, .flatpickr-weekwrapper span.flatpickr-day:hover {
display: block;
width: 100%;
max-width: none;
color: rgba(57,57,57,0.3);
background: transparent;
cursor: default;
border: none;
}
.flatpickr-innerContainer {
display: block;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
}
.flatpickr-rContainer {
display: inline-block;
padding: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.flatpickr-time {
text-align: center;
outline: 0;
display: block;
height: 0;
line-height: 40px;
max-height: 40px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.flatpickr-time:after {
content: "";
display: table;
clear: both;
}
.flatpickr-time .numInputWrapper {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
width: 40%;
height: 40px;
float: left;
}
.flatpickr-time .numInputWrapper span.arrowUp:after {
border-bottom-color: #393939;
}
.flatpickr-time .numInputWrapper span.arrowDown:after {
border-top-color: #393939;
}
.flatpickr-time.hasSeconds .numInputWrapper {
width: 26%;
}
.flatpickr-time.time24hr .numInputWrapper {
width: 49%;
}
.flatpickr-time input {
background: transparent;
-webkit-box-shadow: none;
box-shadow: none;
border: 0;
border-radius: 0;
text-align: center;
margin: 0;
padding: 0;
height: inherit;
line-height: inherit;
color: #393939;
font-size: 14px;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
}
.flatpickr-time input.flatpickr-hour {
font-weight: bold;
}
.flatpickr-time input.flatpickr-minute, .flatpickr-time input.flatpickr-second {
font-weight: 400;
}
.flatpickr-time input:focus {
outline: 0;
border: 0;
}
.flatpickr-time .flatpickr-time-separator, .flatpickr-time .flatpickr-am-pm {
height: inherit;
float: left;
line-height: inherit;
color: #393939;
font-weight: bold;
width: 2%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-align-self: center;
-ms-flex-item-align: center;
align-self: center;
}
.flatpickr-time .flatpickr-am-pm {
outline: 0;
width: 18%;
cursor: pointer;
text-align: center;
font-weight: 400;
}
.flatpickr-time input:hover, .flatpickr-time .flatpickr-am-pm:hover, .flatpickr-time input:focus, .flatpickr-time .flatpickr-am-pm:focus {
background: #eee;
}
.flatpickr-input[readonly] {
cursor: pointer;
}
@keyframes fpFadeInDown {
from {
opacity: 0;
-webkit-transform: translate3d(0,-20px,0);
transform: translate3d(0,-20px,0);
}
to {
opacity: 1;
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
}

View File

@@ -0,0 +1,406 @@
/* !
* ClockPicker v0.0.7 for jQuery (http://weareoutman.github.io/clockpicker/)
* Copyright 2014 Wang Shenwei.
* Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE)
*
* Bootstrap v3.1.1 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */
/* Picked from bootstrap: .popover, .btn, .text-primary */
.popover {
position: absolute;
top: 0;
left: 0;
z-index: 1010;
display: none;
max-width: 276px;
padding: 1px;
text-align: left;
white-space: normal;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
}
.popover.top {
margin-top: -10px;
}
.popover.right {
margin-left: 10px;
}
.popover.bottom {
margin-top: 10px;
}
.popover.left {
margin-left: -10px;
}
.popover-title {
padding: 8px 14px;
margin: 0;
font-size: 14px;
font-weight: normal;
line-height: 18px;
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0;
}
.popover-content {
padding: 9px 14px;
}
.popover > .arrow, .popover > .arrow:after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
overflow: visible;
margin: 0;
padding: 0;
z-index: auto;
background-color: transparent;
-webkit-box-shadow: none;
box-shadow: none;
bottom: auto;
left: auto;
right: auto;
top: auto;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
.popover > .arrow {
border-width: 11px;
}
.popover > .arrow:after {
content: "";
border-width: 10px;
}
.popover.top > .arrow {
bottom: -11px;
left: 50%;
margin-left: -11px;
border-top-color: #999;
border-top-color: rgba(0, 0, 0, .25);
border-bottom-width: 0;
}
.popover.top > .arrow:after {
bottom: 1px;
margin-left: -10px;
content: " ";
border-top-color: #fff;
border-bottom-width: 0;
}
.popover.right > .arrow {
top: 50%;
left: -11px;
margin-top: -11px;
border-right-color: #999;
border-right-color: rgba(0, 0, 0, .25);
border-left-width: 0;
}
.popover.right > .arrow:after {
bottom: -10px;
left: 1px;
content: " ";
border-right-color: #fff;
border-left-width: 0;
}
.popover.bottom > .arrow {
top: -11px;
left: 50%;
margin-left: -11px;
border-top-width: 0;
border-bottom-color: #999;
border-bottom-color: rgba(0, 0, 0, .25);
}
.popover.bottom > .arrow:after {
top: 1px;
margin-left: -10px;
content: " ";
border-top-width: 0;
border-bottom-color: #fff;
}
.popover.left > .arrow {
top: 50%;
right: -11px;
margin-top: -11px;
border-right-width: 0;
border-left-color: #999;
border-left-color: rgba(0, 0, 0, .25);
}
.popover.left > .arrow:after {
right: 1px;
bottom: -10px;
content: " ";
border-right-width: 0;
border-left-color: #fff;
}
.btn {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
}
.btn:focus, .btn:active:focus, .btn.active:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
.btn:hover, .btn:focus {
color: #333;
text-decoration: none;
}
.btn:active, .btn.active {
background-image: none;
outline: 0;
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-default {
color: #333;
background-color: #fff;
border-color: #ccc;
}
.btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open .dropdown-toggle.btn-default {
color: #333;
background-color: #ebebeb;
border-color: #adadad;
}
.btn-default:active, .btn-default.active, .open .dropdown-toggle.btn-default {
background-image: none;
}
.btn-block {
display: block;
width: 100%;
}
.text-primary {
color: #428bca;
}
/* !
* ClockPicker v{package.version} for Bootstrap (http://weareoutman.github.io/clockpicker/)
* Copyright 2014 Wang Shenwei.
* Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) */
.clockpicker .input-group-addon {
cursor: pointer;
}
.clockpicker-moving {
cursor: move;
}
.clockpicker-align-left.popover > .arrow {
left: 25px;
}
.clockpicker-align-top.popover > .arrow {
top: 17px;
}
.clockpicker-align-right.popover > .arrow {
left: auto;
right: 25px;
}
.clockpicker-align-bottom.popover > .arrow {
top: auto;
bottom: 6px;
}
.clockpicker-popover .popover-title {
background-color: #fff;
color: #999;
font-size: 24px;
font-weight: bold;
line-height: 30px;
text-align: center;
}
.clockpicker-popover .popover-title span {
cursor: pointer;
}
.clockpicker-popover .popover-content {
background-color: #f8f8f8;
padding: 12px;
}
.popover-content:last-child {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.clockpicker-plate {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 50%;
width: 200px;
height: 200px;
overflow: visible;
position: relative;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.clockpicker-canvas, .clockpicker-dial {
width: 200px;
height: 200px;
position: absolute;
left: -1px;
top: -1px;
}
.clockpicker-minutes {
visibility: hidden;
}
.clockpicker-tick {
border-radius: 50%;
color: #666;
line-height: 26px;
text-align: center;
width: 26px;
height: 26px;
position: absolute;
cursor: pointer;
}
.clockpicker-tick.active, .clockpicker-tick:hover {
background-color: rgb(192, 229, 247);
background-color: rgba(0, 149, 221, .25);
}
.clockpicker-button {
background-image: none;
background-color: #fff;
border-width: 1px 0 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
margin: 0;
padding: 10px 0;
}
.clockpicker-button:hover {
background-image: none;
background-color: #ebebeb;
}
.clockpicker-button:focus {
outline: none!important;
}
.clockpicker-dial {
-webkit-transition: -webkit-transform 350ms, opacity 350ms;
-moz-transition: -moz-transform 350ms, opacity 350ms;
-ms-transition: -ms-transform 350ms, opacity 350ms;
-o-transition: -o-transform 350ms, opacity 350ms;
transition: transform 350ms, opacity 350ms;
}
.clockpicker-dial-out {
opacity: 0;
}
.clockpicker-hours.clockpicker-dial-out {
-webkit-transform: scale(1.2, 1.2);
-moz-transform: scale(1.2, 1.2);
-ms-transform: scale(1.2, 1.2);
-o-transform: scale(1.2, 1.2);
transform: scale(1.2, 1.2);
}
.clockpicker-minutes.clockpicker-dial-out {
-webkit-transform: scale(.8, .8);
-moz-transform: scale(.8, .8);
-ms-transform: scale(.8, .8);
-o-transform: scale(.8, .8);
transform: scale(.8, .8);
}
.clockpicker-canvas {
-webkit-transition: opacity 175ms;
-moz-transition: opacity 175ms;
-ms-transition: opacity 175ms;
-o-transition: opacity 175ms;
transition: opacity 175ms;
}
.clockpicker-canvas-out {
opacity: 0.25;
}
.clockpicker-canvas-bearing, .clockpicker-canvas-fg {
stroke: none;
fill: rgb(0, 149, 221);
}
.clockpicker-canvas-bg {
stroke: none;
fill: rgb(192, 229, 247);
}
.clockpicker-canvas-bg-trans {
fill: rgba(0, 149, 221, .25);
}
.clockpicker-canvas line {
stroke: rgb(0, 149, 221);
stroke-width: 1;
stroke-linecap: round;
/*shape-rendering: crispEdges;*/
}
.clockpicker-button.am-button {
margin: 1px;
padding: 5px;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 4px;
}
.clockpicker-button.pm-button {
margin: 1px 1px 1px 136px;
padding: 5px;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 4px;
}

View File

@@ -0,0 +1,431 @@
/* !
*
* ../css/litepicker.css
* Litepicker v2.0.12 (https://github.com/wakirin/Litepicker)
* Package: litepicker (https://www.npmjs.com/package/litepicker)
* License: MIT (https://github.com/wakirin/Litepicker/blob/master/LICENCE.md)
* Copyright 2019-2021 Rinat G.
*
* Hash: 2f11f1f0300ea13b17b5
* */
:root {
--litepicker-container-months-color-bg: #fff;
--litepicker-container-months-box-shadow-color: #ddd;
--litepicker-footer-color-bg: #fafafa;
--litepicker-footer-box-shadow-color: #ddd;
--litepicker-tooltip-color-bg: #fff;
--litepicker-month-header-color: #333;
--litepicker-button-prev-month-color: #9e9e9e;
--litepicker-button-next-month-color: #9e9e9e;
--litepicker-button-prev-month-color-hover: #2196f3;
--litepicker-button-next-month-color-hover: #2196f3;
--litepicker-month-width: calc(var(--litepicker-day-width) * 7);
--litepicker-month-weekday-color: #9e9e9e;
--litepicker-month-week-number-color: #9e9e9e;
--litepicker-day-width: 38px;
--litepicker-day-color: #333;
--litepicker-day-color-hover: #2196f3;
--litepicker-is-today-color: #f44336;
--litepicker-is-in-range-color: #bbdefb;
--litepicker-is-locked-color: #9e9e9e;
--litepicker-is-start-color: #fff;
--litepicker-is-start-color-bg: #2196f3;
--litepicker-is-end-color: #fff;
--litepicker-is-end-color-bg: #2196f3;
--litepicker-button-cancel-color: #fff;
--litepicker-button-cancel-color-bg: #9e9e9e;
--litepicker-button-apply-color: #fff;
--litepicker-button-apply-color-bg: #2196f3;
--litepicker-button-reset-color: #909090;
--litepicker-button-reset-color-hover: #2196f3;
--litepicker-highlighted-day-color: #333;
--litepicker-highlighted-day-color-bg: #ffeb3b;
}
.show-week-numbers {
--litepicker-month-width: calc(var(--litepicker-day-width) * 8);
}
.litepicker {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 0.8em;
display: none;
}
.litepicker button {
border: none;
background: none;
}
.litepicker .container__main {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.litepicker .container__months {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
background-color: var(--litepicker-container-months-color-bg);
border-radius: 5px;
-webkit-box-shadow: 0 0 5px var(--litepicker-container-months-box-shadow-color);
box-shadow: 0 0 5px var(--litepicker-container-months-box-shadow-color);
width: calc(var(--litepicker-month-width) + 10px);
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
.litepicker .container__months.columns-2 {
width: calc((var(--litepicker-month-width) * 2) + 20px);
}
.litepicker .container__months.columns-3 {
width: calc((var(--litepicker-month-width) * 3) + 30px);
}
.litepicker .container__months.columns-4 {
width: calc((var(--litepicker-month-width) * 4) + 40px);
}
.litepicker .container__months.split-view .month-item-header .button-previous-month, .litepicker .container__months.split-view .month-item-header .button-next-month {
visibility: visible;
}
.litepicker .container__months .month-item {
padding: 5px;
width: var(--litepicker-month-width);
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
.litepicker .container__months .month-item-header {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
font-weight: 500;
padding: 10px 5px;
text-align: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: var(--litepicker-month-header-color);
}
.litepicker .container__months .month-item-header div {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
.litepicker .container__months .month-item-header div > .month-item-name {
margin-right: 5px;
}
.litepicker .container__months .month-item-header div > .month-item-year {
padding: 0;
}
.litepicker .container__months .month-item-header .reset-button {
color: var(--litepicker-button-reset-color);
}
.litepicker .container__months .month-item-header .reset-button > svg {
fill: var(--litepicker-button-reset-color);
}
.litepicker .container__months .month-item-header .reset-button * {
pointer-events: none;
}
.litepicker .container__months .month-item-header .reset-button:hover {
color: var(--litepicker-button-reset-color-hover);
}
.litepicker .container__months .month-item-header .reset-button:hover > svg {
fill: var(--litepicker-button-reset-color-hover);
}
.litepicker .container__months .month-item-header .button-previous-month, .litepicker .container__months .month-item-header .button-next-month {
visibility: hidden;
text-decoration: none;
padding: 3px 5px;
border-radius: 3px;
-webkit-transition: color 0.3s, border 0.3s;
transition: color 0.3s, border 0.3s;
cursor: default;
}
.litepicker .container__months .month-item-header .button-previous-month *, .litepicker .container__months .month-item-header .button-next-month * {
pointer-events: none;
}
.litepicker .container__months .month-item-header .button-previous-month {
color: var(--litepicker-button-prev-month-color);
}
.litepicker .container__months .month-item-header .button-previous-month > svg, .litepicker .container__months .month-item-header .button-previous-month > img {
fill: var(--litepicker-button-prev-month-color);
}
.litepicker .container__months .month-item-header .button-previous-month:hover {
color: var(--litepicker-button-prev-month-color-hover);
}
.litepicker .container__months .month-item-header .button-previous-month:hover > svg {
fill: var(--litepicker-button-prev-month-color-hover);
}
.litepicker .container__months .month-item-header .button-next-month {
color: var(--litepicker-button-next-month-color);
}
.litepicker .container__months .month-item-header .button-next-month > svg, .litepicker .container__months .month-item-header .button-next-month > img {
fill: var(--litepicker-button-next-month-color);
}
.litepicker .container__months .month-item-header .button-next-month:hover {
color: var(--litepicker-button-next-month-color-hover);
}
.litepicker .container__months .month-item-header .button-next-month:hover > svg {
fill: var(--litepicker-button-next-month-color-hover);
}
.litepicker .container__months .month-item-weekdays-row {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
justify-self: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
color: var(--litepicker-month-weekday-color);
}
.litepicker .container__months .month-item-weekdays-row > div {
padding: 5px 0;
font-size: 85%;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
width: var(--litepicker-day-width);
text-align: center;
}
.litepicker .container__months .month-item:first-child .button-previous-month {
visibility: visible;
}
.litepicker .container__months .month-item:last-child .button-next-month {
visibility: visible;
}
.litepicker .container__months .month-item.no-previous-month .button-previous-month {
visibility: hidden;
}
.litepicker .container__months .month-item.no-next-month .button-next-month {
visibility: hidden;
}
.litepicker .container__days {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
justify-self: center;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: center;
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
.litepicker .container__days > div, .litepicker .container__days > a {
padding: 5px 0;
width: var(--litepicker-day-width);
}
.litepicker .container__days .day-item {
color: var(--litepicker-day-color);
text-align: center;
text-decoration: none;
border-radius: 3px;
-webkit-transition: color 0.3s, border 0.3s;
transition: color 0.3s, border 0.3s;
cursor: default;
}
.litepicker .container__days .day-item:hover {
color: var(--litepicker-day-color-hover);
-webkit-box-shadow: inset 0 0 0 1px var(--litepicker-day-color-hover);
box-shadow: inset 0 0 0 1px var(--litepicker-day-color-hover);
}
.litepicker .container__days .day-item.is-today {
color: var(--litepicker-is-today-color);
}
.litepicker .container__days .day-item.is-locked {
color: var(--litepicker-is-locked-color);
}
.litepicker .container__days .day-item.is-locked:hover {
color: var(--litepicker-is-locked-color);
-webkit-box-shadow: none;
box-shadow: none;
cursor: default;
}
.litepicker .container__days .day-item.is-in-range {
background-color: var(--litepicker-is-in-range-color);
border-radius: 0;
}
.litepicker .container__days .day-item.is-start-date {
color: var(--litepicker-is-start-color);
background-color: var(--litepicker-is-start-color-bg);
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.litepicker .container__days .day-item.is-start-date.is-flipped {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.litepicker .container__days .day-item.is-end-date {
color: var(--litepicker-is-end-color);
background-color: var(--litepicker-is-end-color-bg);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.litepicker .container__days .day-item.is-end-date.is-flipped {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.litepicker .container__days .day-item.is-start-date.is-end-date {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.litepicker .container__days .day-item.is-highlighted {
color: var(--litepicker-highlighted-day-color);
background-color: var(--litepicker-highlighted-day-color-bg);
}
.litepicker .container__days .week-number {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
color: var(--litepicker-month-week-number-color);
font-size: 85%;
}
.litepicker .container__footer {
text-align: right;
padding: 10px 5px;
margin: 0 5px;
background-color: var(--litepicker-footer-color-bg);
-webkit-box-shadow: inset 0px 3px 3px 0px var(--litepicker-footer-box-shadow-color);
box-shadow: inset 0px 3px 3px 0px var(--litepicker-footer-box-shadow-color);
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.litepicker .container__footer .preview-date-range {
margin-right: 10px;
font-size: 90%;
}
.litepicker .container__footer .button-cancel {
background-color: var(--litepicker-button-cancel-color-bg);
color: var(--litepicker-button-cancel-color);
border: 0;
padding: 3px 7px 4px;
border-radius: 3px;
}
.litepicker .container__footer .button-cancel * {
pointer-events: none;
}
.litepicker .container__footer .button-apply {
background-color: var(--litepicker-button-apply-color-bg);
color: var(--litepicker-button-apply-color);
border: 0;
padding: 3px 7px 4px;
border-radius: 3px;
margin-left: 10px;
margin-right: 10px;
}
.litepicker .container__footer .button-apply:disabled {
opacity: 0.7;
}
.litepicker .container__footer .button-apply * {
pointer-events: none;
}
.litepicker .container__tooltip {
position: absolute;
margin-top: -4px;
padding: 4px 8px;
border-radius: 4px;
background-color: var(--litepicker-tooltip-color-bg);
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.25);
box-shadow: 0 1px 3px rgba(0,0,0,0.25);
white-space: nowrap;
font-size: 11px;
pointer-events: none;
visibility: hidden;
}
.litepicker .container__tooltip:before {
position: absolute;
bottom: -5px;
left: calc(50% - 5px);
border-top: 5px solid rgba(0,0,0,0.12);
border-right: 5px solid transparent;
border-left: 5px solid transparent;
content: "";
}
.litepicker .container__tooltip:after {
position: absolute;
bottom: -4px;
left: calc(50% - 4px);
border-top: 4px solid var(--litepicker-tooltip-color-bg);
border-right: 4px solid transparent;
border-left: 4px solid transparent;
content: "";
}

View File

@@ -0,0 +1,978 @@
/*!
* ClockPicker v0.2.3 original by (http://weareoutman.github.io/clockpicker/)
* Copyright 2014 Wang Shenwei.
* Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE)
* Bootstrap 4 support by djibe
*/
(function($) {
var $win = $(window),
$doc = $(document),
$body;
// Can I use inline svg ?
var svgNS = "http://www.w3.org/2000/svg",
svgSupported =
"SVGAngle" in window &&
(function() {
var supported,
el = document.createElement("div");
el.innerHTML = "<svg/>";
supported = (el.firstChild && el.firstChild.namespaceURI) == svgNS;
el.innerHTML = "";
return supported;
})();
// Can I use transition ?
var transitionSupported = (function() {
var style = document.createElement("div").style;
return (
"transition" in style ||
"WebkitTransition" in style ||
"MozTransition" in style ||
"msTransition" in style ||
"OTransition" in style
);
})();
// Listen touch events in touch screen device, instead of mouse events in desktop.
var touchSupported = "ontouchstart" in window,
mousedownEvent = "mousedown" + (touchSupported ? " touchstart" : ""),
mousemoveEvent =
"mousemove.clockpicker" +
(touchSupported ? " touchmove.clockpicker" : ""),
mouseupEvent =
"mouseup.clockpicker" + (touchSupported ? " touchend.clockpicker" : "");
// Vibrate the device if supported
var vibrate = navigator.vibrate
? "vibrate"
: navigator.webkitVibrate
? "webkitVibrate"
: null;
function createSvgElement(name) {
return document.createElementNS(svgNS, name);
}
function leadingZero(num) {
return (num < 10 ? "0" : "") + num;
}
// Get a unique id
var idCounter = 0;
function uniqueId(prefix) {
var id = ++idCounter + "";
return prefix ? prefix + id : id;
}
// Clock size
var dialRadius = 100,
outerRadius = 80,
// innerRadius = 80 on 12 hour clock
innerRadius = 54,
tickRadius = 13;
(diameter = dialRadius * 2), (duration = transitionSupported ? 350 : 1);
// Popover template
var tpl = [
'<div class="popover clockpicker-popover">',
'<div class="arrow"></div>',
'<div class="popover-header">',
'<span class="clockpicker-span-hours"></span>',
":",
'<span class="clockpicker-span-minutes text-white-50"></span>',
'<span class="clockpicker-buttons-am-pm"></span>',
"</div>",
'<div class="popover-body">',
'<div class="clockpicker-plate">',
'<div class="clockpicker-canvas"></div>',
'<div class="clockpicker-dial clockpicker-hours"></div>',
'<div class="clockpicker-dial clockpicker-minutes clockpicker-dial-out"></div>',
"</div>",
'<div class="clockpicker-close-block justify-content-end"></div>',
"</div>",
"</div>"
].join("");
// ClockPicker
function ClockPicker(element, options) {
var popover = $(tpl),
plate = popover.find(".clockpicker-plate"),
hoursView = popover.find(".clockpicker-hours"),
minutesView = popover.find(".clockpicker-minutes"),
isInput = element.prop("tagName") === "INPUT",
input = isInput ? element : element.find("input"),
isHTML5 = input.prop("type") === "time",
addon = element.find(".input-group-addon"),
popoverBody = popover.find(".popover-body"),
closeBlock = popoverBody.find(".clockpicker-close-block"),
self = this,
timer;
this.id = uniqueId("cp");
this.element = element;
this.options = options;
this.options.hourstep = this.parseStep(this.options.hourstep, 12);
this.options.minutestep = this.parseStep(this.options.minutestep, 60);
this.isAppended = false;
this.isShown = false;
this.currentView = "hours";
this.isInput = isInput;
this.isHTML5 = isHTML5;
this.input = input;
this.addon = addon;
this.popover = popover;
this.plate = plate;
this.hoursView = hoursView;
this.minutesView = minutesView;
this.spanHours = popover.find(".clockpicker-span-hours");
this.spanMinutes = popover.find(".clockpicker-span-minutes");
this.buttonsAmPm = popover.find(".clockpicker-buttons-am-pm");
this.currentPlacementClass = options.placement;
this.raiseCallback = function() {
raiseCallback.apply(self, arguments);
};
// Setup for for 12 hour clock if option is selected
if (options.twelvehour) {
$(this.buttonsAmPm).css("display", "flex");
$('<a class="btn-am">AM</a>')
.on("click", function() {
self.amOrPm = "AM";
$(this).removeClass("text-white-50");
$(".btn-pm").addClass("text-white-50");
if (options.ampmSubmit) {
setTimeout(function() {
self.done();
}, duration / 2);
}
})
.appendTo(this.buttonsAmPm);
$('<a class="btn-pm text-white-50">PM</a>')
.on("click", function() {
self.amOrPm = "PM";
$(this).removeClass("text-white-50");
$(".btn-am").addClass("text-white-50");
if (options.ampmSubmit) {
setTimeout(function() {
self.done();
}, duration / 2);
}
})
.appendTo(this.buttonsAmPm);
}
if (!options.autoclose) {
// If autoclose is not setted, append a button
closeBlock
.append(
'<button type="button" class="btn btn-sm btn-outline-primary cancel">' +
options.canceltext +
"</button>"
)
.on("click", ".cancel", function () {
self.hide();
});
closeBlock
.css("display", "flex")
.append(
'<button type="button" class="btn btn-sm btn-outline-primary done">' +
options.donetext +
"</button>"
)
.on("click", ".done", $.proxy(this.done, this));
}
// Placement and arrow align - make sure they make sense.
if (
/^(top|bottom)/.test(options.placement) &&
(options.align === "top" || options.align === "bottom")
)
options.align = "left";
if (
(options.placement === "left" || options.placement === "right") &&
(options.align === "left" || options.align === "right")
)
options.align = "top";
popover.addClass(options.placement);
popover.addClass("clockpicker-align-" + options.align);
this.spanHours.click($.proxy(this.toggleView, this, "hours"));
this.spanMinutes.click($.proxy(this.toggleView, this, "minutes"));
// Show or toggle
if (!options.addonOnly) {
input.on("focus.clockpicker click.clockpicker", $.proxy(this.show, this));
}
addon.on("click.clockpicker", $.proxy(this.toggle, this));
// Build ticks
var tickTpl = $('<div class="clockpicker-tick"></div>'),
i,
tick,
radian,
radius;
// Hours view
if (options.twelvehour) {
for (i = 0; i < 12; i += options.hourstep) {
tick = tickTpl.clone();
radian = (i / 6) * Math.PI;
radius = outerRadius;
tick.css("font-size", "120%");
tick.css({
left: dialRadius + Math.sin(radian) * radius - tickRadius,
top: dialRadius - Math.cos(radian) * radius - tickRadius
});
tick.html(i === 0 ? 12 : i);
hoursView.append(tick);
tick.on(mousedownEvent, mousedown);
}
} else {
for (i = 0; i < 24; i += options.hourstep) {
var isDisabled = false;
if (
options.disabledhours &&
$.inArray(i, options.disabledhours) != -1
) {
var isDisabled = true;
}
tick = tickTpl.clone();
radian = (i / 6) * Math.PI;
var inner = i > 0 && i < 13;
radius = inner ? innerRadius : outerRadius;
tick.css({
left: dialRadius + Math.sin(radian) * radius - tickRadius,
top: dialRadius - Math.cos(radian) * radius - tickRadius
});
if (inner) {
tick.css("font-size", "120%");
}
if (isDisabled) {
tick.addClass("disabled");
}
tick.html(i === 0 ? "00" : i);
hoursView.append(tick);
if (!isDisabled) {
tick.on(mousedownEvent, mousedown);
}
}
}
// Minutes view
var incrementValue = Math.max(options.minutestep, 5);
for (i = 0; i < 60; i += incrementValue) {
tick = tickTpl.clone();
radian = (i / 30) * Math.PI;
tick.css({
left: dialRadius + Math.sin(radian) * outerRadius - tickRadius,
top: dialRadius - Math.cos(radian) * outerRadius - tickRadius
});
tick.css("font-size", "120%");
tick.html(leadingZero(i));
minutesView.append(tick);
tick.on(mousedownEvent, mousedown);
}
// Clicking on minutes view space
plate.on(mousedownEvent, function(e) {
if ($(e.target).closest(".clockpicker-tick").length === 0) {
mousedown(e, true);
}
});
// Mousedown or touchstart
function mousedown(e, space) {
var offset = plate.offset(),
isTouch = /^touch/.test(e.type),
x0 = offset.left + dialRadius,
y0 = offset.top + dialRadius,
dx = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0,
dy = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0,
z = Math.sqrt(dx * dx + dy * dy),
moved = false;
// When clicking on minutes view space, check the mouse position
if (
space &&
(z < outerRadius - tickRadius || z > outerRadius + tickRadius)
) {
return;
}
e.preventDefault();
// Set cursor style of body after 200ms
var movingTimer = setTimeout(function() {
$body.addClass("clockpicker-moving");
}, 200);
// Place the canvas to top
if (svgSupported) {
plate.append(self.canvas);
}
// Clock
self.setHand(dx, dy, true);
// Mousemove on document
$doc.off(mousemoveEvent).on(mousemoveEvent, function(e) {
e.preventDefault();
var isTouch = /^touch/.test(e.type),
x = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0,
y = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0;
if (!moved && x === dx && y === dy) {
// Clicking in chrome on windows will trigger a mousemove event
return;
}
moved = true;
self.setHand(x, y, true);
});
// Mouseup on document
$doc.off(mouseupEvent).on(mouseupEvent, function(e) {
$doc.off(mouseupEvent);
e.preventDefault();
var isTouch = /^touch/.test(e.type),
x = (isTouch ? e.originalEvent.changedTouches[0] : e).pageX - x0,
y = (isTouch ? e.originalEvent.changedTouches[0] : e).pageY - y0;
if ((space || moved) && x === dx && y === dy) {
self.setHand(x, y);
}
if (self.currentView === "hours") {
self.toggleView("minutes", duration / 2);
} else {
if (options.autoclose) {
if (!options.ampmSubmit) {
self.minutesView.addClass("clockpicker-dial-out");
setTimeout(function() {
self.done();
}, duration / 2);
}
}
}
plate.prepend(canvas);
// Reset cursor style of body
clearTimeout(movingTimer);
$body.removeClass("clockpicker-moving");
// Unbind mousemove event
$doc.off(mousemoveEvent);
});
}
if (svgSupported) {
// Draw clock hands and others
var canvas = popover.find(".clockpicker-canvas"),
svg = createSvgElement("svg");
svg.setAttribute("class", "clockpicker-svg");
svg.setAttribute("width", diameter);
svg.setAttribute("height", diameter);
var g = createSvgElement("g");
g.setAttribute(
"transform",
"translate(" + dialRadius + "," + dialRadius + ")"
);
var bearing = createSvgElement("circle");
bearing.setAttribute("class", "clockpicker-canvas-bearing");
bearing.setAttribute("cx", 0);
bearing.setAttribute("cy", 0);
bearing.setAttribute("r", 3);
var hand = createSvgElement("line");
hand.setAttribute("x1", 0);
hand.setAttribute("y1", 0);
var bg = createSvgElement("circle");
bg.setAttribute("class", "clockpicker-canvas-bg");
bg.setAttribute("r", tickRadius);
var fg = createSvgElement("circle");
fg.setAttribute("class", "clockpicker-canvas-fg");
fg.setAttribute("r", 3.5);
g.appendChild(hand);
g.appendChild(bg);
g.appendChild(fg);
g.appendChild(bearing);
svg.appendChild(g);
canvas.append(svg);
this.hand = hand;
this.bg = bg;
this.fg = fg;
this.bearing = bearing;
this.g = g;
this.canvas = canvas;
}
this.raiseCallback(this.options.init, "init");
}
function raiseCallback(callbackFunction, triggerName) {
if (
callbackFunction &&
typeof callbackFunction === "function" &&
this.element
) {
var time = this.getTime() || null;
callbackFunction.call(this.element, time);
}
if (triggerName) {
this.element.trigger("clockpicker." + triggerName || "NoName");
}
}
/**
* Find most suitable vertical placement, doing our best to ensure it is inside of the viewport.
*
* First try to place the element according with preferredPlacement, then try the opposite
* placement and as a last resort, popover will be placed on the very top of the viewport.
*
* @param {jQuery} element
* @param {jQuery} popover
* @param preferredPlacement Preferred placement, if there is enough room for it.
* @returns {string} One of: 'top', 'bottom' or 'viewport-top'.
*/
function resolveAdaptiveVerticalPlacement(
element,
popover,
preferredPlacement
) {
var popoverHeight = popover.outerHeight(),
elementHeight = element.outerHeight(),
elementTopOffset = element.offset().top,
elementBottomOffset = element.offset().top + elementHeight,
minVisibleY = elementTopOffset - element[0].getBoundingClientRect().top,
maxVisibleY = minVisibleY + document.documentElement.clientHeight,
isEnoughRoomAbove = elementTopOffset - popoverHeight >= minVisibleY,
isEnoughRoomBelow = elementBottomOffset + popoverHeight <= maxVisibleY;
if (preferredPlacement === "top") {
if (isEnoughRoomAbove) {
return "top";
} else if (isEnoughRoomBelow) {
return "bottom";
}
} else {
if (isEnoughRoomBelow) {
return "bottom";
} else if (isEnoughRoomAbove) {
return "top";
}
}
return "viewport-top";
}
ClockPicker.prototype.parseStep = function(givenStepSize, wholeSize) {
return wholeSize % givenStepSize === 0 ? givenStepSize : 1;
};
// Default options
ClockPicker.DEFAULTS = {
default: "", // default time, 'now' or '13:14' e.g.
fromnow: 0, // set default time to * milliseconds from now (using with default = 'now')
placement: "bottom", // clock popover placement
align: "left", // popover arrow align
donetext: "OK", // done button text
canceltext: "Cancel", // cancel button text
autoclose: false, // auto close when minute is selected
twelvehour: false, // change to 12 hour AM/PM clock from 24 hour
vibrate: true, // vibrate the device when dragging clock hand
hourstep: 1, // allow to multi increment the hour
minutestep: 1, // allow to multi increment the minute
ampmSubmit: false, // allow submit with AM and PM buttons instead of the minute selection/picker
addonOnly: false, // only open on clicking on the input-addon
disabledhours: null // disabled hours (only 24 hour mode)
};
// Show or hide popover
ClockPicker.prototype.toggle = function() {
this[this.isShown ? "hide" : "show"]();
};
// Set new placement class for popover and remove the old one, if any.
ClockPicker.prototype.updatePlacementClass = function(newClass) {
if (this.currentPlacementClass) {
this.popover.removeClass(this.currentPlacementClass);
}
if (newClass) {
this.popover.addClass(newClass);
}
this.currentPlacementClass = newClass;
};
// Set popover position and update placement class, if needed
ClockPicker.prototype.locate = function() {
var element = this.element,
popover = this.popover,
offset = element.offset(),
width = element.outerWidth(),
height = element.outerHeight(),
placement = this.options.placement,
align = this.options.align,
windowHeight = $win.height(),
windowWidth = $win.width(),
popoverHeight = popover.height(),
popoverWidth = popover.width(),
styles = {},
self = this;
if (placement === "top-adaptive" || placement === "bottom-adaptive") {
var preferredPlacement = placement.substr(0, placement.indexOf("-"));
// Adaptive placement should be resolved into one of the "static" placement
// options, that is best suitable for the current window scroll position.
placement = resolveAdaptiveVerticalPlacement(
element,
popover,
preferredPlacement
);
this.updatePlacementClass(placement !== "viewport-top" ? placement : "");
}
popover.show();
// Place the popover
switch (placement) {
case "bottom":
styles.top = offset.top + height;
break;
case "right":
styles.left = offset.left + width;
break;
case "top":
styles.top = offset.top - popover.outerHeight();
break;
case "left":
styles.left = offset.left - popover.outerWidth();
break;
case "viewport-top":
styles.top = offset.top - element[0].getBoundingClientRect().top;
break;
}
// Align the popover arrow
switch (align) {
case "left":
styles.left = offset.left;
break;
case "right":
styles.left = offset.left + width - popover.outerWidth();
break;
case "top":
styles.top = offset.top;
break;
case "bottom":
styles.top = offset.top + height - popover.outerHeight();
break;
}
// Correct the popover position outside the window
if (popoverHeight + styles.top > windowHeight) {
styles.top = windowHeight - popoverHeight;
}
if (popoverWidth + styles.left > windowWidth) {
styles.left = windowWidth - popoverWidth;
}
popover.css(styles);
};
// The input can be changed by the user
// So before we can use this.hours/this.minutes we must update it
ClockPicker.prototype.parseInputValue = function() {
var value = this.input.prop("value") || this.options["default"] || "";
if (value === "now") {
value = new Date(+new Date() + this.options.fromnow);
}
if (value instanceof Date) {
value = value.getHours() + ":" + value.getMinutes();
}
value = value.split(":");
// Minutes can have AM/PM that needs to be removed
this.hours = +value[0] || 0;
this.minutes = +(value[1] + "").replace(/\D/g, "") || 0;
this.hours =
Math.round(this.hours / this.options.hourstep) * this.options.hourstep;
this.minutes =
Math.round(this.minutes / this.options.minutestep) *
this.options.minutestep;
if (this.options.twelvehour) {
var period = (value[1] + "").replace(/\d+/g, "").toLowerCase();
//this.amOrPm = this.hours > 12 || period === "pm" ? "PM" : "AM";
this.amOrPm = this.hours < 12 || period === "am" ? "AM" : "PM";
}
};
// Show popover
ClockPicker.prototype.show = function(e) {
// Not show again
if (this.isShown) {
return;
}
this.raiseCallback(this.options.beforeShow, "beforeShow");
var self = this;
// Initialize
if (!this.isAppended) {
// Append popover to body
$body = $(document.body).append(this.popover);
// Reset position when resize
$win.on("resize.clockpicker" + this.id, function() {
if (self.isShown) {
self.locate();
}
});
this.isAppended = true;
}
// Get the time from the input field
this.parseInputValue();
this.spanHours.html(leadingZero(this.hours));
this.spanMinutes.html(leadingZero(this.minutes));
// Toggle to hours view
this.toggleView("hours");
// Set position
this.locate();
this.isShown = true;
// Hide when clicking or tabbing on any element except the clock, input and addon
$doc.on(
"click.clockpicker." + this.id + " focusin.clockpicker." + this.id,
function(e) {
var target = $(e.target);
if (
target.closest(self.popover).length === 0 &&
target.closest(self.addon).length === 0 &&
target.closest(self.input).length === 0
) {
self.hide();
}
}
);
// Hide when ESC is pressed
$doc.on("keyup.clockpicker." + this.id, function(e) {
if (e.keyCode === 27) {
self.hide();
}
});
this.raiseCallback(this.options.afterShow, "afterShow");
};
// Hide popover
ClockPicker.prototype.hide = function() {
this.raiseCallback(this.options.beforeHide, "beforeHide");
this.isShown = false;
// Unbinding events on document
$doc.off(
"click.clockpicker." + this.id + " focusin.clockpicker." + this.id
);
$doc.off("keyup.clockpicker." + this.id);
this.popover.hide();
this.raiseCallback(this.options.afterHide, "afterHide");
};
// Toggle to hours or minutes view
ClockPicker.prototype.toggleView = function(view, delay) {
var raiseAfterHourSelect = false;
if (
view === "minutes" &&
$(this.hoursView).css("visibility") === "visible"
) {
this.raiseCallback(this.options.beforeHourSelect, "beforeHourSelect");
raiseAfterHourSelect = true;
}
var isHours = view === "hours",
nextView = isHours ? this.hoursView : this.minutesView,
hideView = isHours ? this.minutesView : this.hoursView;
this.currentView = view;
this.spanHours.toggleClass("text-white-50", !isHours);
this.spanMinutes.toggleClass("text-white-50", isHours);
// Let's make transitions
hideView.addClass("clockpicker-dial-out");
nextView.css("visibility", "visible").removeClass("clockpicker-dial-out");
// Reset clock hand
this.resetClock(delay);
// After transitions ended
clearTimeout(this.toggleViewTimer);
this.toggleViewTimer = setTimeout(function() {
hideView.css("visibility", "hidden");
}, duration);
if (raiseAfterHourSelect) {
this.raiseCallback(this.options.afterHourSelect, "afterHourSelect");
}
};
// Reset clock hand
ClockPicker.prototype.resetClock = function(delay) {
var view = this.currentView,
value = this[view],
isHours = view === "hours",
unit = Math.PI / (isHours ? 6 : 30),
radian = value * unit,
radius = isHours && value > 0 && value < 13 ? innerRadius : outerRadius,
x = Math.sin(radian) * radius,
y = -Math.cos(radian) * radius,
self = this;
if (svgSupported && delay) {
self.canvas.addClass("clockpicker-canvas-out");
setTimeout(function() {
self.canvas.removeClass("clockpicker-canvas-out");
self.setHand(x, y);
}, delay);
} else {
this.setHand(x, y);
}
};
// Set clock hand to (x, y)
ClockPicker.prototype.setHand = function(x, y, dragging) {
var radian = Math.atan2(x, -y),
isHours = this.currentView === "hours",
z = Math.sqrt(x * x + y * y),
options = this.options,
inner = isHours && z < (outerRadius + innerRadius) / 2,
radius = inner ? innerRadius : outerRadius,
unit,
value;
// Calculate the unit
if (isHours) {
unit = (options.hourstep / 6) * Math.PI;
} else {
unit = (options.minutestep / 30) * Math.PI;
}
if (options.twelvehour) {
radius = outerRadius;
}
// Radian should in range [0, 2PI]
if (radian < 0) {
radian = Math.PI * 2 + radian;
}
// Get the round value
value = Math.round(radian / unit);
// Get the round radian
radian = value * unit;
// Correct the hours or minutes
if (isHours) {
value *= options.hourstep;
if (!options.twelvehour && !inner == value > 0) {
value += 12;
}
if (options.twelvehour && value === 0) {
value = 12;
}
if (value === 24) {
value = 0;
}
if (
dragging &&
!options.twelvehour &&
options.disabledhours &&
$.inArray(value, options.disabledhours) != -1
) {
return;
}
} else {
value *= options.minutestep;
if (value === 60) {
value = 0;
}
}
// Once hours or minutes changed, vibrate the device
if (this[this.currentView] !== value) {
if (vibrate && this.options.vibrate) {
// Do not vibrate too frequently
if (!this.vibrateTimer) {
navigator[vibrate](10);
this.vibrateTimer = setTimeout(
$.proxy(function() {
this.vibrateTimer = null;
}, this),
100
);
}
}
}
this[this.currentView] = value;
this[isHours ? "spanHours" : "spanMinutes"].html(leadingZero(value));
// If svg is not supported, just add an active class to the tick
if (!svgSupported) {
this[isHours ? "hoursView" : "minutesView"]
.find(".clockpicker-tick")
.each(function() {
var tick = $(this);
tick.toggleClass("active", value === +tick.html());
});
return;
}
// Place clock hand at the top when dragging
if (dragging || (!isHours && value % 5)) {
this.g.insertBefore(this.hand, this.bearing);
this.g.insertBefore(this.bg, this.fg);
this.bg.setAttribute(
"class",
"clockpicker-canvas-bg clockpicker-canvas-bg-trans"
);
} else {
// Or place it at the bottom
this.g.insertBefore(this.hand, this.bg);
this.g.insertBefore(this.fg, this.bg);
this.bg.setAttribute("class", "clockpicker-canvas-bg");
}
// Set clock hand and others' position
var cx = Math.sin(radian) * radius,
cy = -Math.cos(radian) * radius;
this.hand.setAttribute("x2", cx);
this.hand.setAttribute("y2", cy);
this.bg.setAttribute("cx", cx);
this.bg.setAttribute("cy", cy);
this.fg.setAttribute("cx", cx);
this.fg.setAttribute("cy", cy);
};
// Allow user to get time time as Date object
ClockPicker.prototype.getTime = function(callback) {
var hours = this.hours;
if (this.options.twelvehour && hours < 12 && this.amOrPm === "PM") {
hours += 12;
}
var selectedTime = new Date();
selectedTime.setMinutes(this.minutes);
selectedTime.setHours(hours);
selectedTime.setSeconds(0);
return (
(callback && callback.apply(this.element, selectedTime)) || selectedTime
);
};
// Hours and minutes are selected
ClockPicker.prototype.done = function() {
this.raiseCallback(this.options.beforeDone, "beforeDone");
this.hide();
var last = this.input.prop("value"),
outHours = this.hours,
value = ":" + leadingZero(this.minutes);
if (this.isHTML5 && this.options.twelvehour) {
if (this.hours < 12 && this.amOrPm === "PM") {
outHours += 12;
}
if (this.hours === 12 && this.amOrPm === "AM") {
outHours = 0;
}
}
value = leadingZero(outHours) + value;
if (!this.isHTML5 && this.options.twelvehour) {
value = value + this.amOrPm;
}
this.input.prop("value", value);
if (value !== last) {
this.input.trigger("change");
if (!this.isInput) {
this.element.trigger("change");
}
}
if (this.options.autoclose) {
this.input.trigger("blur");
}
this.raiseCallback(this.options.afterDone, "afterDone");
};
// Remove clockpicker from input
ClockPicker.prototype.remove = function() {
this.element.removeData("clockpicker");
this.input.off("focus.clockpicker click.clockpicker");
this.addon.off("click.clockpicker");
if (this.isShown) {
this.hide();
}
if (this.isAppended) {
$win.off("resize.clockpicker" + this.id);
this.popover.remove();
}
};
// Extends $.fn.clockpicker
$.fn.clockpicker = function(option) {
var args = Array.prototype.slice.call(arguments, 1);
function handleClockPickerRequest() {
var $this = $(this),
data = $this.data("clockpicker");
if (!data) {
var options = $.extend(
{},
ClockPicker.DEFAULTS,
$this.data(),
typeof option == "object" && option
);
$this.data("clockpicker", new ClockPicker($this, options));
} else {
// Manual operations. show, hide, remove, getTime, e.g.
if (typeof data[option] === "function") {
return data[option].apply(data, args);
}
}
}
// If we explicitly do a call on a single element then we can return the value (if needed)
// This allows us, for example, to return the value of getTime
if (this.length == 1) {
var returnValue = handleClockPickerRequest.apply(this[0]);
// If we do not have any return value then return the object itself so you can chain
return returnValue !== undefined ? returnValue : this;
}
// If we do have a list then we do not care about return values
return this.each(handleClockPickerRequest);
};
})(jQuery);

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,729 @@
/*!
* ClockPicker v0.0.7 (http://weareoutman.github.io/clockpicker/)
* Copyright 2014 Wang Shenwei.
* Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE)
*/
;(function(){
var $ = window.jQuery,
$win = $(window),
$doc = $(document),
$body;
// Can I use inline svg ?
var svgNS = 'http://www.w3.org/2000/svg',
svgSupported = 'SVGAngle' in window && (function(){
var supported,
el = document.createElement('div');
el.innerHTML = '<svg/>';
supported = (el.firstChild && el.firstChild.namespaceURI) == svgNS;
el.innerHTML = '';
return supported;
})();
// Can I use transition ?
var transitionSupported = (function(){
var style = document.createElement('div').style;
return 'transition' in style ||
'WebkitTransition' in style ||
'MozTransition' in style ||
'msTransition' in style ||
'OTransition' in style;
})();
// Listen touch events in touch screen device, instead of mouse events in desktop.
var touchSupported = 'ontouchstart' in window,
mousedownEvent = 'mousedown' + ( touchSupported ? ' touchstart' : ''),
mousemoveEvent = 'mousemove.clockpicker' + ( touchSupported ? ' touchmove.clockpicker' : ''),
mouseupEvent = 'mouseup.clockpicker' + ( touchSupported ? ' touchend.clockpicker' : '');
// Vibrate the device if supported
var vibrate = navigator.vibrate ? 'vibrate' : navigator.webkitVibrate ? 'webkitVibrate' : null;
function createSvgElement(name) {
return document.createElementNS(svgNS, name);
}
function leadingZero(num) {
return (num < 10 ? '0' : '') + num;
}
// Get a unique id
var idCounter = 0;
function uniqueId(prefix) {
var id = ++idCounter + '';
return prefix ? prefix + id : id;
}
// Clock size
var dialRadius = 100,
outerRadius = 80,
// innerRadius = 80 on 12 hour clock
innerRadius = 54,
tickRadius = 13,
diameter = dialRadius * 2,
duration = transitionSupported ? 350 : 1;
// Popover template
var tpl = [
'<div class="popover clockpicker-popover">',
'<div class="arrow"></div>',
'<div class="popover-title">',
'<span class="clockpicker-span-hours text-primary"></span>',
' : ',
'<span class="clockpicker-span-minutes"></span>',
'<span class="clockpicker-span-am-pm"></span>',
'</div>',
'<div class="popover-content">',
'<div class="clockpicker-plate">',
'<div class="clockpicker-canvas"></div>',
'<div class="clockpicker-dial clockpicker-hours"></div>',
'<div class="clockpicker-dial clockpicker-minutes clockpicker-dial-out"></div>',
'</div>',
'<span class="clockpicker-am-pm-block">',
'</span>',
'</div>',
'</div>'
].join('');
// ClockPicker
function ClockPicker(element, options) {
var popover = $(tpl),
plate = popover.find('.clockpicker-plate'),
hoursView = popover.find('.clockpicker-hours'),
minutesView = popover.find('.clockpicker-minutes'),
amPmBlock = popover.find('.clockpicker-am-pm-block'),
isInput = element.prop('tagName') === 'INPUT',
input = isInput ? element : element.find('input'),
addon = element.find('.input-group-addon'),
self = this,
timer;
this.id = uniqueId('cp');
this.element = element;
this.options = options;
this.isAppended = false;
this.isShown = false;
this.currentView = 'hours';
this.isInput = isInput;
this.input = input;
this.addon = addon;
this.popover = popover;
this.plate = plate;
this.hoursView = hoursView;
this.minutesView = minutesView;
this.amPmBlock = amPmBlock;
this.spanHours = popover.find('.clockpicker-span-hours');
this.spanMinutes = popover.find('.clockpicker-span-minutes');
this.spanAmPm = popover.find('.clockpicker-span-am-pm');
this.amOrPm = "PM";
// Setup for for 12 hour clock if option is selected
if (options.twelvehour) {
var amPmButtonsTemplate = ['<div class="clockpicker-am-pm-block">',
'<button type="button" class="btn btn-sm btn-default clockpicker-button clockpicker-am-button">',
'AM</button>',
'<button type="button" class="btn btn-sm btn-default clockpicker-button clockpicker-pm-button">',
'PM</button>',
'</div>'].join('');
var amPmButtons = $(amPmButtonsTemplate);
//amPmButtons.appendTo(plate);
////Not working b/c they are not shown when this runs
//$('clockpicker-am-button')
// .on("click", function() {
// self.amOrPm = "AM";
// $('.clockpicker-span-am-pm').empty().append('AM');
// });
//
//$('clockpicker-pm-button')
// .on("click", function() {
// self.amOrPm = "PM";
// $('.clockpicker-span-am-pm').empty().append('PM');
// });
$('<button type="button" class="btn btn-sm btn-default clockpicker-button am-button">' + "AM" + '</button>')
.on("click", function() {
self.amOrPm = "AM";
$('.clockpicker-span-am-pm').empty().append('AM');
}).appendTo(this.amPmBlock);
$('<button type="button" class="btn btn-sm btn-default clockpicker-button pm-button">' + "PM" + '</button>')
.on("click", function() {
self.amOrPm = 'PM';
$('.clockpicker-span-am-pm').empty().append('PM');
}).appendTo(this.amPmBlock);
}
if (! options.autoclose) {
// If autoclose is not setted, append a button
$('<button type="button" class="btn btn-sm btn-default btn-block clockpicker-button">' + options.donetext + '</button>')
.click($.proxy(this.done, this))
.appendTo(popover);
}
// Placement and arrow align - make sure they make sense.
if ((options.placement === 'top' || options.placement === 'bottom') && (options.align === 'top' || options.align === 'bottom')) options.align = 'left';
if ((options.placement === 'left' || options.placement === 'right') && (options.align === 'left' || options.align === 'right')) options.align = 'top';
popover.addClass(options.placement);
popover.addClass('clockpicker-align-' + options.align);
this.spanHours.click($.proxy(this.toggleView, this, 'hours'));
this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes'));
// Show or toggle
input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this));
addon.on('click.clockpicker', $.proxy(this.toggle, this));
// Build ticks
var tickTpl = $('<div class="clockpicker-tick"></div>'),
i, tick, radian, radius;
// Hours view
if (options.twelvehour) {
for (i = 1; i < 13; i += 1) {
tick = tickTpl.clone();
radian = i / 6 * Math.PI;
radius = outerRadius;
tick.css('font-size', '120%');
tick.css({
left: dialRadius + Math.sin(radian) * radius - tickRadius,
top: dialRadius - Math.cos(radian) * radius - tickRadius
});
tick.html(i === 0 ? '00' : i);
hoursView.append(tick);
tick.on(mousedownEvent, mousedown);
}
} else {
for (i = 0; i < 24; i += 1) {
tick = tickTpl.clone();
radian = i / 6 * Math.PI;
var inner = i > 0 && i < 13;
radius = inner ? innerRadius : outerRadius;
tick.css({
left: dialRadius + Math.sin(radian) * radius - tickRadius,
top: dialRadius - Math.cos(radian) * radius - tickRadius
});
if (inner) {
tick.css('font-size', '120%');
}
tick.html(i === 0 ? '00' : i);
hoursView.append(tick);
tick.on(mousedownEvent, mousedown);
}
}
// Minutes view
for (i = 0; i < 60; i += 5) {
tick = tickTpl.clone();
radian = i / 30 * Math.PI;
tick.css({
left: dialRadius + Math.sin(radian) * outerRadius - tickRadius,
top: dialRadius - Math.cos(radian) * outerRadius - tickRadius
});
tick.css('font-size', '120%');
tick.html(leadingZero(i));
minutesView.append(tick);
tick.on(mousedownEvent, mousedown);
}
// Clicking on minutes view space
plate.on(mousedownEvent, function(e){
if ($(e.target).closest('.clockpicker-tick').length === 0) {
mousedown(e, true);
}
});
// Mousedown or touchstart
function mousedown(e, space) {
var offset = plate.offset(),
isTouch = /^touch/.test(e.type),
x0 = offset.left + dialRadius,
y0 = offset.top + dialRadius,
dx = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0,
dy = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0,
z = Math.sqrt(dx * dx + dy * dy),
moved = false;
// When clicking on minutes view space, check the mouse position
if (space && (z < outerRadius - tickRadius || z > outerRadius + tickRadius)) {
return;
}
e.preventDefault();
// Set cursor style of body after 200ms
var movingTimer = setTimeout(function(){
$body.addClass('clockpicker-moving');
}, 200);
// Place the canvas to top
if (svgSupported) {
plate.append(self.canvas);
}
// Clock
self.setHand(dx, dy, ! space, true);
// Mousemove on document
$doc.off(mousemoveEvent).on(mousemoveEvent, function(e){
e.preventDefault();
var isTouch = /^touch/.test(e.type),
x = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0,
y = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0;
if (! moved && x === dx && y === dy) {
// Clicking in chrome on windows will trigger a mousemove event
return;
}
moved = true;
self.setHand(x, y, false, true);
});
// Mouseup on document
$doc.off(mouseupEvent).on(mouseupEvent, function(e){
$doc.off(mouseupEvent);
e.preventDefault();
var isTouch = /^touch/.test(e.type),
x = (isTouch ? e.originalEvent.changedTouches[0] : e).pageX - x0,
y = (isTouch ? e.originalEvent.changedTouches[0] : e).pageY - y0;
if ((space || moved) && x === dx && y === dy) {
self.setHand(x, y);
}
if (self.currentView === 'hours') {
self.toggleView('minutes', duration / 2);
} else {
if (options.autoclose) {
self.minutesView.addClass('clockpicker-dial-out');
setTimeout(function(){
self.done();
}, duration / 2);
}
}
plate.prepend(canvas);
// Reset cursor style of body
clearTimeout(movingTimer);
$body.removeClass('clockpicker-moving');
// Unbind mousemove event
$doc.off(mousemoveEvent);
});
}
if (svgSupported) {
// Draw clock hands and others
var canvas = popover.find('.clockpicker-canvas'),
svg = createSvgElement('svg');
svg.setAttribute('class', 'clockpicker-svg');
svg.setAttribute('width', diameter);
svg.setAttribute('height', diameter);
var g = createSvgElement('g');
g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')');
var bearing = createSvgElement('circle');
bearing.setAttribute('class', 'clockpicker-canvas-bearing');
bearing.setAttribute('cx', 0);
bearing.setAttribute('cy', 0);
bearing.setAttribute('r', 2);
var hand = createSvgElement('line');
hand.setAttribute('x1', 0);
hand.setAttribute('y1', 0);
var bg = createSvgElement('circle');
bg.setAttribute('class', 'clockpicker-canvas-bg');
bg.setAttribute('r', tickRadius);
var fg = createSvgElement('circle');
fg.setAttribute('class', 'clockpicker-canvas-fg');
fg.setAttribute('r', 3.5);
g.appendChild(hand);
g.appendChild(bg);
g.appendChild(fg);
g.appendChild(bearing);
svg.appendChild(g);
canvas.append(svg);
this.hand = hand;
this.bg = bg;
this.fg = fg;
this.bearing = bearing;
this.g = g;
this.canvas = canvas;
}
raiseCallback(this.options.init);
}
function raiseCallback(callbackFunction) {
if (callbackFunction && typeof callbackFunction === "function") {
callbackFunction();
}
}
// Default options
ClockPicker.DEFAULTS = {
'default': '', // default time, 'now' or '13:14' e.g.
fromnow: 0, // set default time to * milliseconds from now (using with default = 'now')
placement: 'bottom', // clock popover placement
align: 'left', // popover arrow align
donetext: '完成', // done button text
autoclose: false, // auto close when minute is selected
twelvehour: false, // change to 12 hour AM/PM clock from 24 hour
vibrate: true // vibrate the device when dragging clock hand
};
// Show or hide popover
ClockPicker.prototype.toggle = function(){
this[this.isShown ? 'hide' : 'show']();
};
// Set popover position
ClockPicker.prototype.locate = function(){
var element = this.element,
popover = this.popover,
offset = element.offset(),
width = element.outerWidth(),
height = element.outerHeight(),
placement = this.options.placement,
align = this.options.align,
styles = {},
self = this;
popover.show();
// Place the popover
switch (placement) {
case 'bottom':
styles.top = offset.top + height;
break;
case 'right':
styles.left = offset.left + width;
break;
case 'top':
styles.top = offset.top - popover.outerHeight();
break;
case 'left':
styles.left = offset.left - popover.outerWidth();
break;
}
// Align the popover arrow
switch (align) {
case 'left':
styles.left = offset.left;
break;
case 'right':
styles.left = offset.left + width - popover.outerWidth();
break;
case 'top':
styles.top = offset.top;
break;
case 'bottom':
styles.top = offset.top + height - popover.outerHeight();
break;
}
popover.css(styles);
};
// Show popover
ClockPicker.prototype.show = function(e){
// Not show again
if (this.isShown) {
return;
}
raiseCallback(this.options.beforeShow);
var self = this;
// Initialize
if (! this.isAppended) {
// Append popover to body
$body = $(document.body).append(this.popover);
// Reset position when resize
$win.on('resize.clockpicker' + this.id, function(){
if (self.isShown) {
self.locate();
}
});
this.isAppended = true;
}
// Get the time
var value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':');
if (value[0] === 'now') {
var now = new Date(+ new Date() + this.options.fromnow);
value = [
now.getHours(),
now.getMinutes()
];
}
this.hours = + value[0] || 0;
this.minutes = + value[1] || 0;
this.spanHours.html(leadingZero(this.hours));
this.spanMinutes.html(leadingZero(this.minutes));
// Toggle to hours view
this.toggleView('hours');
// Set position
this.locate();
this.isShown = true;
// Hide when clicking or tabbing on any element except the clock, input and addon
$doc.on('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id, function(e){
var target = $(e.target);
if (target.closest(self.popover).length === 0 &&
target.closest(self.addon).length === 0 &&
target.closest(self.input).length === 0) {
self.hide();
}
});
// Hide when ESC is pressed
$doc.on('keyup.clockpicker.' + this.id, function(e){
if (e.keyCode === 27) {
self.hide();
}
});
raiseCallback(this.options.afterShow);
};
// Hide popover
ClockPicker.prototype.hide = function(){
raiseCallback(this.options.beforeHide);
this.isShown = false;
// Unbinding events on document
$doc.off('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id);
$doc.off('keyup.clockpicker.' + this.id);
this.popover.hide();
raiseCallback(this.options.afterHide);
};
// Toggle to hours or minutes view
ClockPicker.prototype.toggleView = function(view, delay){
var raiseAfterHourSelect = false;
if (view === 'minutes' && $(this.hoursView).css("visibility") === "visible") {
raiseCallback(this.options.beforeHourSelect);
raiseAfterHourSelect = true;
}
var isHours = view === 'hours',
nextView = isHours ? this.hoursView : this.minutesView,
hideView = isHours ? this.minutesView : this.hoursView;
this.currentView = view;
this.spanHours.toggleClass('text-primary', isHours);
this.spanMinutes.toggleClass('text-primary', ! isHours);
// Let's make transitions
hideView.addClass('clockpicker-dial-out');
nextView.css('visibility', 'visible').removeClass('clockpicker-dial-out');
// Reset clock hand
this.resetClock(delay);
// After transitions ended
clearTimeout(this.toggleViewTimer);
this.toggleViewTimer = setTimeout(function(){
hideView.css('visibility', 'hidden');
}, duration);
if (raiseAfterHourSelect) {
raiseCallback(this.options.afterHourSelect);
}
};
// Reset clock hand
ClockPicker.prototype.resetClock = function(delay){
var view = this.currentView,
value = this[view],
isHours = view === 'hours',
unit = Math.PI / (isHours ? 6 : 30),
radian = value * unit,
radius = isHours && value > 0 && value < 13 ? innerRadius : outerRadius,
x = Math.sin(radian) * radius,
y = - Math.cos(radian) * radius,
self = this;
if (svgSupported && delay) {
self.canvas.addClass('clockpicker-canvas-out');
setTimeout(function(){
self.canvas.removeClass('clockpicker-canvas-out');
self.setHand(x, y);
}, delay);
} else {
this.setHand(x, y);
}
};
// Set clock hand to (x, y)
ClockPicker.prototype.setHand = function(x, y, roundBy5, dragging){
var radian = Math.atan2(x, - y),
isHours = this.currentView === 'hours',
unit = Math.PI / (isHours || roundBy5 ? 6 : 30),
z = Math.sqrt(x * x + y * y),
options = this.options,
inner = isHours && z < (outerRadius + innerRadius) / 2,
radius = inner ? innerRadius : outerRadius,
value;
if (options.twelvehour) {
radius = outerRadius;
}
// Radian should in range [0, 2PI]
if (radian < 0) {
radian = Math.PI * 2 + radian;
}
// Get the round value
value = Math.round(radian / unit);
// Get the round radian
radian = value * unit;
// Correct the hours or minutes
if (options.twelvehour) {
if (isHours) {
if (value === 0) {
value = 12;
}
} else {
if (roundBy5) {
value *= 5;
}
if (value === 60) {
value = 0;
}
}
} else {
if (isHours) {
if (value === 12) {
value = 0;
}
value = inner ? (value === 0 ? 12 : value) : value === 0 ? 0 : value + 12;
} else {
if (roundBy5) {
value *= 5;
}
if (value === 60) {
value = 0;
}
}
}
// Once hours or minutes changed, vibrate the device
if (this[this.currentView] !== value) {
if (vibrate && this.options.vibrate) {
// Do not vibrate too frequently
if (! this.vibrateTimer) {
navigator[vibrate](10);
this.vibrateTimer = setTimeout($.proxy(function(){
this.vibrateTimer = null;
}, this), 100);
}
}
}
this[this.currentView] = value;
this[isHours ? 'spanHours' : 'spanMinutes'].html(leadingZero(value));
// If svg is not supported, just add an active class to the tick
if (! svgSupported) {
this[isHours ? 'hoursView' : 'minutesView'].find('.clockpicker-tick').each(function(){
var tick = $(this);
tick.toggleClass('active', value === + tick.html());
});
return;
}
// Place clock hand at the top when dragging
if (dragging || (! isHours && value % 5)) {
this.g.insertBefore(this.hand, this.bearing);
this.g.insertBefore(this.bg, this.fg);
this.bg.setAttribute('class', 'clockpicker-canvas-bg clockpicker-canvas-bg-trans');
} else {
// Or place it at the bottom
this.g.insertBefore(this.hand, this.bg);
this.g.insertBefore(this.fg, this.bg);
this.bg.setAttribute('class', 'clockpicker-canvas-bg');
}
// Set clock hand and others' position
var cx = Math.sin(radian) * radius,
cy = - Math.cos(radian) * radius;
this.hand.setAttribute('x2', cx);
this.hand.setAttribute('y2', cy);
this.bg.setAttribute('cx', cx);
this.bg.setAttribute('cy', cy);
this.fg.setAttribute('cx', cx);
this.fg.setAttribute('cy', cy);
};
// Hours and minutes are selected
ClockPicker.prototype.done = function() {
raiseCallback(this.options.beforeDone);
this.hide();
var last = this.input.prop('value'),
value = leadingZero(this.hours) + ':' + leadingZero(this.minutes);
if (this.options.twelvehour) {
value = value + this.amOrPm;
}
this.input.prop('value', value);
if (value !== last) {
this.input.triggerHandler('change');
if (! this.isInput) {
this.element.trigger('change');
}
}
if (this.options.autoclose) {
this.input.trigger('blur');
}
raiseCallback(this.options.afterDone);
};
// Remove clockpicker from input
ClockPicker.prototype.remove = function() {
this.element.removeData('clockpicker');
this.input.off('focus.clockpicker click.clockpicker');
this.addon.off('click.clockpicker');
if (this.isShown) {
this.hide();
}
if (this.isAppended) {
$win.off('resize.clockpicker' + this.id);
this.popover.remove();
}
};
// Extends $.fn.clockpicker
$.fn.clockpicker = function(option){
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function(){
var $this = $(this),
data = $this.data('clockpicker');
if (! data) {
var options = $.extend({}, ClockPicker.DEFAULTS, $this.data(), typeof option == 'object' && option);
$this.data('clockpicker', new ClockPicker($this, options));
} else {
// Manual operatsions. show, hide, remove, e.g.
if (typeof data[option] === 'function') {
data[option].apply(data, args);
}
}
});
};
}());

View File

@@ -78,6 +78,7 @@ $(document).ready(function () {
$('#languagebanktablebody').empty();
window.selectedlanguagerow = null;
let $btnClear = $('#btnClear');
let $btndefaultinit = $('#btnDefaultInit');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
@@ -86,7 +87,6 @@ $(document).ready(function () {
$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');
@@ -121,18 +121,6 @@ $(document).ready(function () {
$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) => {
@@ -141,8 +129,18 @@ $(document).ready(function () {
}, (errdata) => {
alert("Error clear languageLink : " + errdata.message);
});
});
$btndefaultinit.click(() => {
if (confirm("Default Init will clear existing data and create default language link data. Cotinue ?")){
fetchAPI(APIURL + "DefaultInit", "POST", {}, null, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success default init languageLink : " + okdata.message);
}, (errdata) => {
alert("Error default init languageLink : " + errdata.message);
});
}
});
$btnAdd.click(() => {
// show modal with id 'languagemodal'
$modal.modal('show');

File diff suppressed because one or more lines are too long

View File

@@ -21,7 +21,7 @@ dtLog = null;
function fill_logtablebody(vv) {
dtLog.clear();
if (!Array.isArray(vv) || vv.length === 0) {
$('#btnExport').prop('disabled', true);
//$('#btnExport').prop('disabled', true);
return;
}
dtLog.rows.add(vv);
@@ -29,7 +29,7 @@ function fill_logtablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length);
$('#btnExport').prop('disabled', false);
//$('#btnExport').prop('disabled', false);
}
/**
@@ -43,8 +43,10 @@ function reloadLogs(APIURL = "Log/", date, filter) {
date: date,
filter: filter
})
console.log("Loading logs with params: " + params.toString());
window.logdata = [];
fetchAPI(APIURL + "List?" + params.toString(), "GET", {}, null, (okdata) => {
console.log("Logs loaded: " + okdata.length);
if (Array.isArray(okdata)) {
window.logdata.push(...okdata);
fill_logtablebody(window.logdata);
@@ -54,16 +56,33 @@ function reloadLogs(APIURL = "Log/", date, filter) {
});
}
datepicker = null;
$btnGet = null;
$(document).ready(function () {
console.log("log.js ready");
let selectedlogdate = "";
let logfilter = "";
let APIURL = "Log/";
$btnGet = $('#btnGet');
datepicker = new Litepicker({
element: document.getElementById('logdate'),
format: 'DD/MM/YYYY',
lang: 'en-US',
autoApply: true,
singleMode: true,
startDate: new Date(),
onSelect: (date) => {
selectedlogdate = date.format('DD/MM/YYYY');
console.log("Selected date: " + selectedlogdate);
}
})
if (dtLog === null) {
dtLog = new DataTable('#logtable', {
dom: 'Bfrtip',
data: [],
pageLength: 25,
columns: [
@@ -72,36 +91,42 @@ $(document).ready(function () {
{ title: "Time", data: "timenya" },
{ title: "Machine", data: "machine" },
{ title: "Description", data: "description" }
]
],
buttons: ['print', 'pdf', 'excel']
});
}
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);
// findalldate is checkbox, if checked will disable datepicker
$('#findalldate').off('change').on('change', function () {
if ($(this).is(':checked')) {
datepicker.disabled = true;
selectedlogdate = "alldate";
console.log("Find all date checked, omitting date filter");
} else {
datepicker.disabled = false;
const date = datepicker.getDate();
selectedlogdate = date.format('DD/MM/YYYY');
console.log("Find all date unchecked, selected date: " + selectedlogdate);
}
});
$('#searchfilter').off('input').on('input', function () {
logfilter = $(this).val();
//reloadLogs(APIURL, selectedlogdate, logfilter);
});
$btnGet.click(function () {
let checked = $('#findalldate').is(':checked');
if (checked && logfilter.trim() === "") {
alert("Please enter a filter when 'Find All Date' is checked to avoid large data load.");
return;
}
//$(this).data('selectedlogdate', selectedlogdate);
//$(this).data('logfilter', logfilter);
reloadLogs(APIURL, selectedlogdate, logfilter);
});
$('#btnExport').off('click').on('click', function () {
DoExport(APIURL, "log.xlsx", { date: selectedlogdate, filter: logfilter });
});
selectedlogdate = datepicker.getDate().format('DD/MM/YYYY');
console.log("Initial selected date: " + selectedlogdate);
$btnGet.trigger('click'); // load logs on page load
});

View File

@@ -25,22 +25,38 @@
* @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} filename - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} duration - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} elapsed - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} broadcastzones - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} vu - The jQuery result should be <progress-bar> element.
*/
function getCardByIndex(index) {
let cardname = "ch" + index.toString().padStart(2, '0');
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')}`),
//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')}`),
//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')}`),
//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')}`),
//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`),
//vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`),
card: $(`#${cardname}`),
title: $(`#${cardname} .streamertitle`),
ip: $(`#${cardname} .streamerip`),
buffer: $(`#${cardname} .streamerbuffer`),
status: $(`#${cardname} .streamerstatus`),
vu: $(`#${cardname} .streamervu .progress-bar`),
filename: $(`#${cardname} .streamerfile`),
duration: $(`#${cardname} .streamerduration`),
elapsed: $(`#${cardname} .streamerelapsed`),
broadcastzones: $(`#${cardname} .streamerzones`),
}
return obj;
}
@@ -64,29 +80,71 @@ function UpdateStreamerCard(values) {
values = [];
}
let visiblilitychanged = false;
for (let i = 1; i <= 64; i++) {
let vv = values.find(v => v.index === i);
let card = getCardByIndex(i);
const vv = values.find(v => v.index === i);
const cardname = "ch" + i.toString().padStart(2, '0');
const $card = $(`#${cardname}`);
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' : 'Idle'}`);
if (card.vu) {
setProgress(i, card.vu, vv.vu, 100);
// ada data untuk index i
if ($card.length > 0) {
// ada card untuk index i, show card
if ($card.hasClass('d-none')) {
visiblilitychanged = true;
$card.removeClass('d-none');
$card.closest('.streamercol').removeClass('d-none'); // show the column as well
}
const $title = $(`#${cardname} .streamertitle`);
const $ip = $(`#${cardname} .streamerip`);
const $buffer = $(`#${cardname} .streamerbuffer`);
const $status = $(`#${cardname} .streamerstatus`);
const $vu = $(`#${cardname} .streamervu .progress-bar`);
const $filename = $(`#${cardname} .streamerfile`);
const $duration = $(`#${cardname} .streamerduration`);
const $elapsed = $(`#${cardname} .streamerelapsed`);
const $broadcastzones = $(`#${cardname} .streamerzones`);
//console.log(`Updating card for index ${i}`, vv);
$title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`);
$ip.text(vv.ipaddress ? vv.ipaddress : 'N/A');
$buffer.text(vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A');
$status.text(vv.isPlaying ? 'Playing' : 'Idle');
setProgress(i, $vu, vv.vu ? vv.vu : 0, 100);
$filename.text(vv.filename ? vv.filename : 'N/A');
$duration.text(vv.duration ? vv.duration : 'N/A');
$elapsed.text(vv.elapsed ? vv.elapsed : 'N/A');
$broadcastzones.text(vv.broadcastzones ? vv.broadcastzones : 'N/A');
}
} 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);
// tidak ada data untuk index i, hide card
if ($card.length > 0) {
// ada card untuk index i, hide card
if (!$card.hasClass('d-none')) {
visiblilitychanged = true;
$card.addClass('d-none');
$card.closest('.streamercol').addClass('d-none'); // hide the column as well
}
}
}
}
// hide rows that have all cards hidden
if (visiblilitychanged) {
$('.streamerrow').each(function () {
const $row = $(this);
const visiblecards = $row.find('.streamercard:not(.d-none)');
if (visiblecards.length === 0) {
$row.addClass('d-none');
} else {
$row.removeClass('d-none');
}
});
}
}
/**
@@ -428,51 +486,51 @@ $(document).ready(function () {
runIntervalJob();
window.addEventListener('ws_connected', () =>{
window.addEventListener('ws_connected', () => {
console.log("overview.js ws_connected event triggered");
runIntervalJob();
runIntervalJob();
});
window.addEventListener('ws_disconnected', ()=>{
window.addEventListener('ws_disconnected', () => {
console.log("overview.js ws_disconnected event triggered");
if (intervaljob1) clearInterval(intervaljob1);
if (intervaljob2) clearInterval(intervaljob2);
intervaljob1 = null;
intervaljob2 = null;
if (intervaljob1) clearInterval(intervaljob1);
if (intervaljob2) clearInterval(intervaljob2);
intervaljob1 = null;
intervaljob2 = null;
});
window.addEventListener('ws_message', ()=>{
window.addEventListener('ws_message', () => {
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);
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);
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;
}
let cmd = rep.reply;
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getPagingQueue":
let pq = JSON.parse(data);
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);
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 () {

View File

@@ -22,6 +22,15 @@ window.schedulebankdata = [];
window.selectedschedulerow = null;
dtScheduleBank = null;
dtTodaySchedule = null;
function fill_todayscheduletablebody(vv) {
dtTodaySchedule.clear();
if (!Array.isArray(vv) || vv.length === 0) return;
dtTodaySchedule.rows.add(vv);
dtTodaySchedule.draw();
}
/**
* Fill schedulebank table body with values
@@ -34,7 +43,6 @@ function fill_schedulebanktablebody(vv) {
dtScheduleBank.rows.add(vv);
dtScheduleBank.draw();
$('#schedulebanktable tbody').off('click').on('click', 'tr', function () {
// if no data
if (!dtScheduleBank) return;
@@ -118,6 +126,22 @@ function reloadTimerBank(APIURL = "ScheduleBank/") {
});
}
function reloadTodaySchedule(APIURL = "ScheduleBank/") {
fetchAPI(APIURL + "TodaySchedule", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
console.log("Today's Schedule: ", okdata);
fill_todayscheduletablebody(okdata);
}
}, (errdata) => {
alert("Error loading today's schedule : " + errdata.message);
});
}
dayViewMode = 'all'; // all, everyday, monday, tuesday, wednesday, thursday, friday, saturday, sunday
scheduledate = null; // Litepicker instance for schedule date selection
scheduletime = null; // time picker instance for schedule time selection
$schedulemodal = null; // schedule modal jQuery object
$(document).ready(function () {
console.log("schedulebank.js loaded successfully");
$('#schedulebanktablebody').empty();
@@ -131,9 +155,12 @@ $(document).ready(function () {
$btnEdit.prop('disabled', true);
$btnRemove.prop('disabled', true);
let APIURL = "ScheduleBank/";
if (dtScheduleBank === null) {
dtScheduleBank = new DataTable('#schedulebanktable', {
dom: 'Bfrtip',
data: [],
pageLength: 25,
columns: [
@@ -141,25 +168,99 @@ $(document).ready(function () {
{ title: "Description", data: "description" },
{ title: "Day", data: "day" },
{ title: "Time", data: "time" },
{ title: "Sound Path", data: "soundpath" },
{ title: "Message", data: "soundpath" },
{ title: "Repeat", data: "repeat" },
{ title: "Enable", data: "enable" },
{ title: "Broadcast Zones", data: "broadcastZones" },
{ title: "Language", data: "language" }
]
],
buttons: ['print', 'pdf', {
extend: 'collection',
text: 'View',
className: 'btn btn-outline-secondary',
buttons: [
{text: 'All', className: 'btn btn-outline-primary', action() { dayViewMode = 'all'; dtScheduleBank.draw(); } },
{ text: 'Everyday', className: 'btn btn-outline-primary', action() { dayViewMode = 'everyday'; dtScheduleBank.draw(); } },
{ text: 'Monday', className: 'btn btn-outline-primary', action() { dayViewMode = 'monday'; dtScheduleBank.draw(); } },
{ text: 'Tuesday', className: 'btn btn-outline-primary', action() { dayViewMode = 'tuesday'; dtScheduleBank.draw(); } },
{ text: 'Wednesday', className: 'btn btn-outline-primary', action() { dayViewMode = 'wednesday'; dtScheduleBank.draw(); } },
{ text: 'Thursday', className: 'btn btn-outline-primary', action() { dayViewMode = 'thursday'; dtScheduleBank.draw(); } },
{ text: 'Friday', className: 'btn btn-outline-primary', action() { dayViewMode = 'friday'; dtScheduleBank.draw(); } },
{ text: 'Saturday', className: 'btn btn-outline-primary', action() { dayViewMode = 'saturday'; dtScheduleBank.draw(); } },
{ text: 'Sunday', className: 'btn btn-outline-primary', action() { dayViewMode = 'sunday'; dtScheduleBank.draw(); } },
{text: 'Special Date', className: 'btn btn-outline-primary', action() { dayViewMode = 'specialdate'; dtScheduleBank.draw(); } }
]
}]
});
}
if (dtTodaySchedule === null) {
dtTodaySchedule = new DataTable('#todaytable', {
dom: 'Bfrtip',
data: [],
pageLength: 25,
columns: [
{ title: "No", data: "index" },
{ title: "Description", data: "description" },
{ title: "Day", data: "day" },
{ title: "Time", data: "time" },
{ title: "Message", data: "soundpath" },
{ title: "Repeat", data: "repeat" },
{ title: "Enable", data: "enable" },
{ title: "Broadcast Zones", data: "broadcastZones" },
{ title: "Language", data: "language" }
],
buttons: ['print', 'pdf']
});
}
let $schedulemodal = $('#schedulemodal');
$.fn.dataTable.ext.search.push(function (settings, data, dataIndex, rowData) {
if (settings.nTable.id !== 'schedulebanktable') return true;
switch (dayViewMode) {
case 'everyday':
return rowData.day.toLowerCase() === 'everyday';
case 'monday':
return rowData.day.toLowerCase() === 'monday';
case 'tuesday':
return rowData.day.toLowerCase() === 'tuesday';
case 'wednesday':
return rowData.day.toLowerCase() === 'wednesday';
case 'thursday':
return rowData.day.toLowerCase() === 'thursday';
case 'friday':
return rowData.day.toLowerCase() === 'friday';
case 'saturday':
return rowData.day.toLowerCase() === 'saturday';
case 'sunday':
return rowData.day.toLowerCase() === 'sunday';
case 'specialdate':
// match dd/mm/yyyy format
return /^\d{2}\/\d{2}\/\d{4}$/.test(rowData.day);
default:
// 'all' include in here
return true;
}
})
$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');
//let $schedulehour = $schedulemodal.find('#schedulehour');
// number input 0-59
let $scheduleminute = $schedulemodal.find('#scheduleminute');
//let $scheduleminute = $schedulemodal.find('#scheduleminute');
scheduletime = flatpickr("#scheduletime",{
enableTime: true,
noCalendar: true, // time only
dateFormat: "H:i", // HH:mm format
time_24hr: true, // firce 24-hour format
minuteIncrement: 1,
defaultDate: new Date()
});
// select2 for message
let $schedulemessage = $schedulemodal.find('#schedulemessage');
// number input 0-5
@@ -176,24 +277,41 @@ $(document).ready(function () {
let $weeklyselect = $schedulemodal.find('#weeklyselect');
// radio button for specific date
let $schedulespecialdate = $schedulemodal.find('#schedulespecialdate');
scheduledate = new Litepicker({
element: document.getElementById('scheduledate'),
format: 'DD/MM/YYYY',
lang: 'en-US',
autoApply: true,
singleMode: true,
startDate: new Date(),
onSelect: (date) => {
console.log("Selected special date: " + date.format('DD/MM/YYYY'));
}
})
// date input
let $scheduledate = $schedulemodal.find('#scheduledate');
//let $scheduledate = $schedulemodal.find('#scheduledate');
// select2 for language
let $languageselect = $schedulemodal.find('#languageselect');
$schedulespecialdate.off('change').on('change', function () {
if ($(this).is(':checked')) {
$scheduledate.prop('disabled', false);
//$scheduledate.prop('disabled', false);
scheduledate.disabled = false
} else {
$scheduledate.prop('disabled', true);
//$scheduledate.prop('disabled', true);
scheduledate.disabled = true
}
});
function clearScheduleModal() {
$scheduleid.prop('disabled', true).val('');
$scheduledescription.val('');
$schedulehour.val('0');
$scheduleminute.val('0');
//$schedulehour.val('0');
//$scheduleminute.val('0');
$schedulerepeat.val('1');
$scheduleenable.prop('checked', true);
$scheduleeveryday.prop('checked', false);
@@ -214,7 +332,8 @@ $(document).ready(function () {
dropdownParent: $('#schedulemodal')
});
$scheduledate.prop('disabled', true).val('');
//$scheduledate.prop('disabled', true).val('');
scheduledate.disabled = true;
$schedulezones.empty().select2({
data: window.BroadcastZoneList.map(zone => ({ id: zone.description, text: zone.description })),
placeholder: 'Select broadcast zones',
@@ -237,46 +356,28 @@ $(document).ready(function () {
$scheduleeveryday.off('change').on('change', function () {
if ($(this).is(':checked')) {
$weeklyselect.prop('disabled', true);
$scheduledate.prop('disabled', true);
//$scheduledate.prop('disabled', true);
scheduledate.disabled = true;
}
});
$scheduleweekly.off('change').on('change', function () {
if ($(this).is(':checked')) {
$weeklyselect.prop('disabled', false);
$scheduledate.prop('disabled', true);
} else {
$weeklyselect.prop('disabled', true);
}
//$scheduledate.prop('disabled', true);
scheduledate.disabled = true;
}
});
$schedulespecialdate.off('change').on('change', function () {
if ($(this).is(':checked')) {
$weeklyselect.prop('disabled', true);
$scheduledate.prop('disabled', false);
} else {
$scheduledate.prop('disabled', true);
}
//$scheduledate.prop('disabled', false);
scheduledate.disabled = false;
}
});
}
let $findschedule = $('#findschedule');
// $findschedule.off('input').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);
reloadTodaySchedule(APIURL);
reloadBroadcastZones();
getLanguages();
getScheduledDays();
@@ -284,6 +385,7 @@ $(document).ready(function () {
$btnClear.click(() => {
DoClear(APIURL, "Timerbank", (okdata) => {
reloadTimerBank(APIURL);
reloadTodaySchedule(APIURL);
alert("Success clear schedulebank : " + okdata.message);
}, (errdata) => {
alert("Error clear schedulebank : " + errdata.message);
@@ -292,7 +394,9 @@ $(document).ready(function () {
});
$btnAdd.click(() => {
$schedulemodal.modal('show');
clearScheduleModal();
$scheduleeveryday.prop('checked', true).trigger('click');
$schedulemodal.off('click.scheduleclose').on('click.scheduleclose', '#scheduleclose', function () {
$schedulemodal.modal('hide');
@@ -308,16 +412,16 @@ $(document).ready(function () {
if ($scheduleeveryday.is(':checked')) {
_Day = "Everyday";
} else if ($schedulespecialdate.is(':checked')) {
_Day = Convert_input_date_to_string($scheduledate.val());
_Day = Convert_input_date_to_string(scheduledate.getDate().format('DD/MM/YYYY'));
} else if ($scheduleweekly.is(':checked')) {
_Day = $weeklyselect.val();
}
const Language = $languageselect.val().join(';');
const broadcastZones = $schedulezones.val().join(';');
// 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')}`;
//const hour = parseInt($schedulehour.val(), 10);
//const minute = parseInt($scheduleminute.val(), 10);
const _Time = $('#scheduletime').val();
if (Description.length > 0) {
if (_Day.length > 0) {
if (Message.length > 0) {
@@ -334,10 +438,12 @@ $(document).ready(function () {
BroadcastZones: broadcastZones,
Language: Language
};
console.log("Adding schedule: ", scheduleObj);
fetchAPI(APIURL + "Add", "POST", {}, scheduleObj, (okdata) => {
alert("Success add schedule: " + okdata.message);
reloadTimerBank(APIURL);
reloadTodaySchedule(APIURL);
alert("Success add schedule: " + okdata.message);
}, (errdata) => {
alert("Error add schedule: " + errdata.message);
});
@@ -363,9 +469,11 @@ $(document).ready(function () {
broadcastZones: window.selectedschedulerow.broadcastZones,
language: window.selectedschedulerow.language
}
if (confirm(`Are you sure to delete schedule [${sr.index}] Description=${sr.description}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sr.index, "DELETE", {}, null, (okdata) => {
reloadTimerBank(APIURL);
reloadTodaySchedule(APIURL);
alert("Success delete schedule : " + okdata.message);
}, (errdata) => {
alert("Error delete schedule : " + errdata.message);
@@ -387,6 +495,7 @@ $(document).ready(function () {
BroadcastZones: window.selectedschedulerow.broadcastZones,
Language: window.selectedschedulerow.language
}
console.log("Editing schedule: ", sr);
if (confirm(`Are you sure to edit schedule [${sr.index}] Description=${sr.Description}?`)) {
$schedulemodal.modal('show');
clearScheduleModal();
@@ -395,16 +504,24 @@ $(document).ready(function () {
$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());
//$schedulehour.val(hour.toString());
//$scheduleminute.val(minute.toString());
scheduletime.setDate(sr.Time,true, "H:i");
$schedulemessage.val(sr.Soundpath).trigger('change');
$schedulerepeat.val(sr.Repeat);
$scheduleenable.prop('checked', sr.Enable.toLowerCase() === 'true');
$scheduleenable.prop('checked', sr.Enable);
$languageselect.val(sr.Language.split(';')).trigger('change');
$schedulezones.val(sr.BroadcastZones.split(';')).trigger('change');
switch (sr.Day) {
case 'Everyday':
$scheduleeveryday.click();
// make the everyday radio button checked
$scheduleeveryday.prop('checked', true);
$weeklyselect.val(null).trigger('change');
$weeklyselect.prop('disabled', true);
//$scheduledate.prop('disabled', true);
scheduledate.disabled = true;
break;
case 'Sunday':
case 'Monday':
@@ -413,15 +530,28 @@ $(document).ready(function () {
case 'Thursday':
case 'Friday':
case 'Saturday':
$scheduleweekly.click();
$scheduleweekly.prop('checked', true);
$weeklyselect.val(sr.Day).trigger('change');
$weeklyselect.prop('disabled', false);
//$scheduledate.prop('disabled', true);
scheduledate.disabled = true;
break;
default:
console.log("Assuming special date for Day: ", sr.Day);
// 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(Convert_string_to_input_date(sr.Day));
$schedulespecialdate.prop('checked', true);
//$scheduledate.val(Convert_string_to_input_date(sr.Day));
// $scheduledate.prop('disabled', false);
scheduledate.setDate(dayjs(sr.Day,'DD/MM/YYYY'));
scheduledate.disabled = false;
$weeklyselect.val(null).trigger('change');
$weeklyselect.prop('disabled', true);
}
}
@@ -440,7 +570,8 @@ $(document).ready(function () {
Day = "Everyday";
} else if ($schedulespecialdate.is(':checked')) {
// convert date from yyyy-mm-dd to dd/mm/yyyy
Day = Convert_input_date_to_string($scheduledate.val());
//Day = Convert_input_date_to_string($scheduledate.val());
Day = scheduledate.getDate().format('DD/MM/YYYY');
} else if ($scheduleweekly.is(':checked')) {
Day = $weeklyselect.val();
}
@@ -448,9 +579,9 @@ $(document).ready(function () {
const BroadcastZones = $schedulezones.val().join(';');
const Language = $languageselect.val().join(';');
// 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')}`;
// const hour = parseInt($schedulehour.val(), 10);
//const minute = parseInt($scheduleminute.val(), 10);
const Time = $('#scheduletime').val();
if (Description && Description.length > 0) {
if (Day && Day.length > 0) {
if (Soundpath && Soundpath.length > 0) {
@@ -472,6 +603,7 @@ $(document).ready(function () {
fetchAPI(APIURL + "UpdateByIndex/" + sr.index, "PATCH", {}, scheduleObj, (okdata) => {
alert("Success edit schedule: " + okdata.message);
reloadTimerBank(APIURL);
reloadTodaySchedule(APIURL);
}, (errdata) => {
alert("Error edit schedule: " + errdata.message);
});
@@ -493,6 +625,7 @@ $(document).ready(function () {
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadTimerBank(APIURL);
reloadTodaySchedule(APIURL);
alert("Success import schedulebank from XLSX : " + okdata.message);
}, (errdata) => {
alert("Error importing schedulebank from XLSX : " + errdata.message);

View File

@@ -67,6 +67,275 @@ function load_default_voice(){
});
}
function ValidLatitude(lat){
const num = parseFloat(lat);
return !isNaN(num) && num >= -90 && num <= 90;
}
function ValidLongitude(lon){
const num = parseFloat(lon);
return !isNaN(num) && num >= -180 && num <= 180;
}
function ValidTimezone(tz){
try{
Intl.DateTimeFormat(undefined, { timeZone: tz });
return true;
} catch(e){
return false;
}
}
function ValidHHMM(time){
const regex = /^([01]\d|2[0-3]):([0-5]\d)$/;
return regex.test(time);
}
/**
* Check if a date string is valid in DD/MM/YYYY format
* @param {string} dateStr date to check in DD/MM/YYYY format
* @returns {boolean} true if valid date in DD/MM/YYYY format, false otherwise
*/
function ValidDateDDMMYYYY(dateStr){
const regex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;
if (!regex.test(dateStr)) return false;
const [day, month, year] = dateStr.split('/').map(Number);
const date = new Date(year, month - 1, day);
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
}
function ValidFilePath(path){
if (typeof path !== 'string' || path.trim() === '') return false;
// test if ends with .wav or .mp3
const regex = /\.(wav|mp3)$/i;
return regex.test(path);
}
function IsEnabled(value){
// accept "true", "false", true, false, 1, 0
// detect if value is null or undefined
if (value === null || value === undefined) return false;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value === 1;
if (typeof value === 'string') return value.toLowerCase() === "true" || value === "1";
return false;
}
/**
* @typedef {Object} AdzanSetting
* @property {string} latitude
* @property {string} longitude
* @property {string} timezone
* @property {string} fajar_sound
* @property {string} dzuhur_sound
* @property {string} ashar_sound
* @property {string} maghrib_sound
* @property {string} isya_sound
* @property {string} fajar_time
* @property {string} dzuhur_time
* @property {string} ashar_time
* @property {string} maghrib_time
* @property {string} isya_time
* @property {boolean} fajar_enable
* @property {boolean} dzuhur_enable
* @property {boolean} ashar_enable
* @property {boolean} maghrib_enable
* @property {boolean} isya_enable
*/
function Get_AdzanSetting(){
fetchAPI("Settings/AdzanSetting", "GET", {}, null,
/**
* returned AdzanSetting data
* @param {AdzanSetting} okdata
*/
(okdata) => {
// text input for latitude, longitude, timezone
if (ValidLatitude(okdata.latitude)) {
$('#adzanlatitude').val(okdata.latitude);
} else {
$('#adzanlatitude').val("N/A");
}
if (ValidLongitude(okdata.longitude)) {
$('#adzanlongitude').val(okdata.longitude);
} else {
$('#adzanlongitude').val("N/A");
}
if (ValidTimezone(okdata.timezone)) {
$('#adzantimezone').val(okdata.timezone);
} else {
$('#adzantimezone').val("N/A");
}
// text input for adzan sound file for each prayer time
if (ValidFilePath(okdata.fajar_sound)) {
$('#fajar .adzanfile').val(okdata.fajar_sound);
} else {
$('#fajar .adzanfile').val("N/A");
}
if (ValidFilePath(okdata.dzuhur_sound)) {
$('#dzuhur .adzanfile').val(okdata.dzuhur_sound);
} else {
$('#dzuhur .adzanfile').val("N/A");
}
if (ValidFilePath(okdata.ashar_sound)) {
$('#ashar .adzanfile').val(okdata.ashar_sound);
} else {
$('#ashar .adzanfile').val("N/A");
}
if (ValidFilePath(okdata.maghrib_sound)) {
$('#maghrib .adzanfile').val(okdata.maghrib_sound);
} else {
$('#maghrib .adzanfile').val("N/A");
}
if (ValidFilePath(okdata.isya_sound)) {
$('#isya .adzanfile').val(okdata.isya_sound);
} else {
$('#isya .adzanfile').val("N/A");
}
// checkbox adzanenable for each prayer time
$('#fajar .adzanenable').prop('checked', IsEnabled(okdata.fajar_enable));
$('#dzuhur .adzanenable').prop('checked', IsEnabled(okdata.dzuhur_enable));
$('#ashar .adzanenable').prop('checked', IsEnabled(okdata.ashar_enable));
$('#maghrib .adzanenable').prop('checked', IsEnabled(okdata.maghrib_enable));
$('#isya .adzanenable').prop('checked', IsEnabled(okdata.isya_enable));
// adzantime for each prayer time
// if valid HH:MM will set <input type="time"> value, else set to undefined
if (ValidHHMM(okdata.fajar_time)) {
$('#fajar .adzantime').val(okdata.fajar_time);
} else {
$('#fajar .adzantime').val("");
}
if (ValidHHMM(okdata.dzuhur_time)) {
$('#dzuhur .adzantime').val(okdata.dzuhur_time);
} else {
$('#dzuhur .adzantime').val("");
}
if (ValidHHMM(okdata.ashar_time)) {
$('#ashar .adzantime').val(okdata.ashar_time);
} else {
$('#ashar .adzantime').val("");
}
if (ValidHHMM(okdata.maghrib_time)) {
$('#maghrib .adzantime').val(okdata.maghrib_time);
} else {
$('#maghrib .adzantime').val("");
}
if (ValidHHMM(okdata.isya_time)) {
$('#isya .adzantime').val(okdata.isya_time);
} else {
$('#isya .adzantime').val("");
}
}, (errdata) => {
alert("Error getting Adzan settings : " + errdata.message);
});
}
function Set_AdzanSetting(){
let latitude = $('#adzanlatitude').val();
if (!ValidLatitude(latitude)){
alert("Please enter a valid latitude between -90 and 90.");
return;
}
let longitude = $('#adzanlongitude').val();
if (!ValidLongitude(longitude)){
alert("Please enter a valid longitude between -180 and 180.");
return;
}
let timezone = $('#adzantimezone').val();
if (!ValidTimezone(timezone)){
alert("Please enter a valid timezone.");
return;
}
let fajar_sound = $('#fajar .adzanfile').val();
if (!ValidFilePath(fajar_sound)){
alert("Please enter a valid file path for Fajar adzan sound (must end with .wav or .mp3).");
return;
}
let dzuhur_sound = $('#dzuhur .adzanfile').val();
if (!ValidFilePath(dzuhur_sound)){
alert("Please enter a valid file path for Dzuhur adzan sound (must end with .wav or .mp3).");
return;
}
let ashar_sound = $('#ashar .adzanfile').val();
if (!ValidFilePath(ashar_sound)){
alert("Please enter a valid file path for Ashar adzan sound (must end with .wav or .mp3).");
return;
}
let maghrib_sound = $('#maghrib .adzanfile').val();
if (!ValidFilePath(maghrib_sound)){
alert("Please enter a valid file path for Maghrib adzan sound (must end with .wav or .mp3).");
return;
}
let isya_sound = $('#isya .adzanfile').val();
if (!ValidFilePath(isya_sound)){
alert("Please enter a valid file path for Isya adzan sound (must end with .wav or .mp3).");
return;
}
let fajar_enable = $('#fajar .adzanenable').prop('checked');
let dzuhur_enable = $('#dzuhur .adzanenable').prop('checked');
let ashar_enable = $('#ashar .adzanenable').prop('checked');
let maghrib_enable = $('#maghrib .adzanenable').prop('checked');
let isya_enable = $('#isya .adzanenable').prop('checked');
let fajar_time = $('#fajar .adzantime').val();
if (!ValidHHMM(fajar_time)){
alert("Please enter a valid time for Fajar adzan (HH:MM).");
return;
}
let dzuhur_time = $('#dzuhur .adzantime').val();
if (!ValidHHMM(dzuhur_time)){
alert("Please enter a valid time for Dzuhur adzan (HH:MM).");
return;
}
let ashar_time = $('#ashar .adzantime').val();
if (!ValidHHMM(ashar_time)){
alert("Please enter a valid time for Ashar adzan (HH:MM).");
return;
}
let maghrib_time = $('#maghrib .adzantime').val();
if (!ValidHHMM(maghrib_time)){
alert("Please enter a valid time for Maghrib adzan (HH:MM).");
return;
}
let isya_time = $('#isya .adzantime').val();
if (!ValidHHMM(isya_time)){
alert("Please enter a valid time for Isya adzan (HH:MM).");
return;
}
/**
* @type {AdzanSetting}
*/
let data = {
latitude: latitude,
longitude: longitude,
timezone: timezone,
fajar_sound: fajar_sound,
dzuhur_sound: dzuhur_sound,
ashar_sound: ashar_sound,
maghrib_sound: maghrib_sound,
isya_sound: isya_sound,
fajar_enable: fajar_enable,
dzuhur_enable: dzuhur_enable,
ashar_enable: ashar_enable,
maghrib_enable: maghrib_enable,
isya_enable: isya_enable,
fajar_time: fajar_time,
dzuhur_time: dzuhur_time,
ashar_time: ashar_time,
maghrib_time: maghrib_time,
isya_time: isya_time
};
fetchAPI("Settings/AdzanSetting", "POST", {}, data, (okdata) => {
alert("Adzan settings updated successfully.");
}, (errdata) => {
alert("Error updating Adzan settings : " + errdata.message);
});
}
function Get_WebAccessSetting(){
fetchAPI("Settings/WebAccess", "GET", {}, null, (okdata) => {
let adminpass = okdata.adminpass || "password";
@@ -109,8 +378,10 @@ $(document).ready(function () {
load_default_voice();
Get_OldResultDays();
Get_WebAccessSetting();
Get_AdzanSetting();
load_messagebank(() => load_remark_selection());
$("#fiscodesave").off('click').on('click', function () {
$('#fiscodesave').click(function () {
Set_OldResultDays();
let gop = $("#input_GOP").val();
let gbd = $("#input_GBD").val();
@@ -133,10 +404,13 @@ $(document).ready(function () {
} else {
alert("Please select all FIS codes (GOP, GBD, GFC, FLD) and Default Voice before saving.");
}
});
$("#webaccesssave").off('click').on('click', function () {
Set_WebAccessSetting();
$('#webaccesssave').click(function () {
Set_WebAccessSetting();
});
$('#adzansave').click(function () {
Set_AdzanSetting();
});

View File

@@ -37,6 +37,11 @@ dtSoundbank = null;
*/
window.select2data = [];
$btnRemove = null;
$btnEdit = null;
$btnExport = null;
$btnfilecheck = null;
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
@@ -62,7 +67,18 @@ function reloadSoundBank(APIURL = "SoundBank/") {
*/
function fill_soundbanktablebody(vv) {
dtSoundbank.clear();
if (!Array.isArray(vv) || vv.length === 0) return;
if (!Array.isArray(vv) || vv.length === 0) {
// kalau kosong, tidak bisa remove, edit, export dan filecheck
$btnRemove.prop('disabled', true);
$btnEdit.prop('disabled', true);
$btnExport.prop('disabled', true);
$btnfilecheck.prop('disabled', true);
return;
} else {
// kalau ada isi, bisa export dan filecheck, tapi remove dan edit tetap tergantung selection
$btnExport.prop('disabled', false);
$btnfilecheck.prop('disabled', false);
}
dtSoundbank.rows.add(vv);
dtSoundbank.draw();
@@ -77,8 +93,8 @@ function fill_soundbanktablebody(vv) {
if ($(this).hasClass('row-selected')) {
$(this).removeClass('row-selected').find('td').css('background-color', '');
window.selectedsoundrow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
$btnRemove.prop('disabled', true);
$btnEdit.prop('disabled', true);
return;
}
@@ -87,8 +103,8 @@ function fill_soundbanktablebody(vv) {
$(this).addClass('row-selected').find('td').css('background-color', '#ffeeba');
window.selectedsoundrow = selected.data();
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
$btnRemove.prop('disabled', false);
$btnEdit.prop('disabled', false);
})
$('#tablesize').text("Table Size: " + vv.length);
@@ -146,7 +162,7 @@ function getFilenameFromPath(path) {
}
fileViewMode = 'all'; // all, valid, invalid
$(document).ready(function () {
console.log("soundbank.js loaded successfully");
@@ -154,9 +170,9 @@ $(document).ready(function () {
window.selectedsoundrow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
let $btnExport = $('#btnExport');
$btnRemove = $('#btnRemove');
$btnEdit = $('#btnEdit');
$btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
$btnRemove.prop('disabled', true);
$btnEdit.prop('disabled', true);
@@ -171,9 +187,11 @@ $(document).ready(function () {
let selected_category = null;
let selected_language = null;
let selected_voicetype = null;
$btnfilecheck = $('#btnFileCheck');
if (dtSoundbank === null) {
dtSoundbank = new DataTable('#soundbanktable', {
dom: 'Bfrtip',
data: [],
pageLength: 25,
columns: [
@@ -184,9 +202,29 @@ $(document).ready(function () {
{ title: "Language", data: "language" },
{ title: "Type", data: "voiceType" },
{ title: "Filename", data: "path" }
]
],
rowCallback: function (row, data) {
row.classList.toggle('table-danger', !!data.filemissing);
},
buttons: ['print', 'pdf', {
extend: 'collection',
text: 'View',
className: 'btn btn-outline-secondary',
buttons: [
{ text: 'All Files', className: 'btn btn-outline-primary', action() { fileViewMode = 'all'; dtSoundbank.draw(); } },
{ text: 'Valid Files', className: 'btn btn-outline-success', action() { fileViewMode = 'valid'; dtSoundbank.draw(); } },
{ text: 'Invalid Files', className: 'btn btn-outline-danger', action() { fileViewMode = 'invalid'; dtSoundbank.draw(); } }
]
}]
});
}
$.fn.dataTable.ext.search.push(function (settings, data, dataIndex, rowData) {
if (settings.nTable.id !== 'soundbanktable') return true;
const isInvalid = !!rowData.filemissing;
if (fileViewMode === 'invalid') return isInvalid;
if (fileViewMode === 'valid') return !isInvalid;
return true;
})
/**
@@ -219,17 +257,6 @@ $(document).ready(function () {
}
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);
@@ -447,4 +474,37 @@ $(document).ready(function () {
alert("Error importing soundbank from XLSX : " + errdata.message);
});
});
$btnfilecheck.click(() => {
fetchAPI(APIURL + "FileCheck", "GET", {}, null, (okdata) => {
console.log(okdata);
let invalidList = Array.isArray(okdata.invalidfile) ? okdata.invalidfile : [];
let validList = Array.isArray(okdata.validfile) ? okdata.validfile : [];
const invalidSet = new Set(invalidList.map(f => f.path));
dtSoundbank.rows().every(function () {
const d = this.data();
d.filemissing = invalidSet.has(d.path);
this.data(d); // update row data
});
dtSoundbank.draw(false);
console.log(`File Check completed. ${validList.length} valid files, ${invalidList.length} invalid files.`);
if (validList.length === 0) {
if (invalidList.length === 0) {
alert("No soundbank files found on server.");
} else {
alert(`File Check completed. All ${invalidList.length} soundbank files are missing on server.`);
}
} else {
if (invalidList.length === 0) {
alert(`File Check completed. All ${validList.length} soundbank files are present on server.`);
} else {
alert(`File Check completed. ${validList.length} soundbank files are present, ${invalidList.length} soundbank files are missing on server.`);
}
}
}, (errdata) => {
alert("Error checking soundbank files : " + errdata.message);
});
});
});

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -294,7 +293,6 @@
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/soundchannel.js"></script>
<script src="assets/js/broadcastzones.js"></script>
<script src="assets/js/datatables.js"></script>
</body>
</html>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -156,7 +155,6 @@
<div><audio class="invisible" id="audioplayer" controls=""></audio></div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/filemanagement.js"></script>
</body>

View File

@@ -16,6 +16,8 @@
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/all.min.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/flatpickr.min.css">
<link rel="stylesheet" href="assets/css/litepicker.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">
@@ -40,26 +42,26 @@
<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;">
<li class="nav-item"><a class="nav-link active link-light text-menu" id="homelink" href="#" aria-current="page"><svg class="bi bi-house-door me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" 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 pad-icon-menu" style="font-size: 20px;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="soundbanklink" href="#"><svg class="bi bi-soundwave me-2 icon-menu pad-icon-menu" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" 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;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="messagebanklink" href="#"><svg class="me-2 icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="languagelink" href="#"><svg class="me-2 icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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 pad-icon-menu" style="font-size: 20px;">
<li class="nav-item .icon-menu"><a class="nav-link link-body-emphasis text-menu" id="timerlink" href="#"><svg class="me-2 icon-menu pad-icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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;">
<li class="nav-item .icon-menu"><a class="nav-link link-body-emphasis text-menu" id="broadcastzonelink" href="#"><svg class="me-2 icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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>
@@ -71,11 +73,11 @@
<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 pad-icon-menu" style="font-size: 20px;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="usermanagement" href="#"><svg class="me-2 icon-menu pad-icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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="filemanagement" 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 pad-icon-menu" style="font-size: 20px;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="filemanagement" href="#"><svg class="me-2 icon-menu pad-icon-menu" 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" style="font-size: 20px;">
<g>
<rect fill="none" height="24" width="24"></rect>
</g>
@@ -83,13 +85,13 @@
<path d="M14,2H6C4.9,2,4.01,2.9,4.01,4L4,20c0,1.1,0.89,2,1.99,2H18c1.1,0,2-0.9,2-2V8L14,2z M16,13h-3v3.75 c0,1.24-1.01,2.25-2.25,2.25S8.5,17.99,8.5,16.75c0-1.24,1.01-2.25,2.25-2.25c0.46,0,0.89,0.14,1.25,0.38V11h4V13z M13,9V3.5 L18.5,9H13z"></path>
</g>
</svg>&nbsp;File 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 pad-icon-menu" style="font-size: 20px;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="settinglink" href="#"><svg class="me-2 icon-menu pad-icon-menu" 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" 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;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="logoutlink" href="#"><svg class="me-2 icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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>
@@ -159,10 +161,12 @@
<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/all.min.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/select2.min.js"></script>
<script src="assets/js/litepicker.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/all.min.js"></script>
<script src="assets/js/script.js"></script>
<script src="assets/js/flatpickr.js"></script>
</body>
</html>

View File

@@ -16,6 +16,8 @@
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/all.min.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/flatpickr.min.css">
<link rel="stylesheet" href="assets/css/litepicker.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">
@@ -40,7 +42,7 @@
<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;">
<li class="nav-item"><a class="nav-link active link-light text-menu" id="homelink" href="#" aria-current="page"><svg class="bi bi-house-door me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" 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="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 pad-icon-menu" style="font-size: 20px;">
@@ -49,7 +51,7 @@
<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="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;">
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="logoutlink" href="#"><svg class="me-2 icon-menu" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" 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>
@@ -119,10 +121,12 @@
<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/all.min.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/select2.min.js"></script>
<script src="assets/js/litepicker.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/all.min.js"></script>
<script src="assets/js/script.js"></script>
<script src="assets/js/flatpickr.js"></script>
</body>
</html>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -34,11 +33,12 @@
</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="btnDefaultInit" type="button">Initialize Default</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 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-import" id="btnExport" type="button">Export</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-import" id="btnImport" type="button">Import</button></div>
</div>
<div class="row">
<div class="col">
@@ -99,7 +99,6 @@
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/languagelink.js"></script>
<script src="assets/js/datatables.js"></script>
</body>
</html>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -26,16 +25,26 @@
</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 class="col-5 ms-1 me-1">
<div class="row">
<div class="col-auto">
<p class="text-add">Select Log Date</p>
</div>
<div class="col-auto"><input type="text" id="logdate" class="form-control"></div>
<div class="col">
<div class="form-check w-100 h-100 align-content-center"><input class="form-check-input" type="checkbox" id="findalldate"><label class="form-check-label" for="formCheck-1">All Date</label></div>
</div>
</div>
</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 d-none">
<p class="text-add">Search</p>
<div class="col-5 ms-1 me-1">
<div class="row">
<div class="col-auto">
<p class="text-add">Search</p>
</div>
<div class="col"><input type="text" id="searchfilter" class="form-control" placeholder="Search Filter"></div>
</div>
</div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2 d-none"><input type="text" id="searchfilter" class="form-control" placeholder="Search Filter"></div>
<div class="col ms-1 me-1"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnGet" type="button">Get</button></div>
</div>
<div class="row">
<div class="col">
@@ -61,7 +70,6 @@
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/log.js"></script>
<script src="assets/js/datatables.js"></script>
</body>
</html>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -27,7 +26,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">
<div class="bs-icon-xl bs-icon-circle bs-icon-primary my-4 bs-icon bg-icon-login"><svg class="bi bi-person bg-icon-login" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<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>
@@ -46,8 +45,6 @@
</section>
<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/datatables.js"></script>
<script src="assets/js/login.js"></script>
</body>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -114,16 +113,13 @@
<div class="row">
<div class="col bg-light"><select class="w-100 h-100 overflow-scroll" id="messageavailablevariables" size="10"></select></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. -->
<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. -->
<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. -->
<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>
@@ -137,7 +133,6 @@
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/messagebank.js"></script>
<script src="assets/js/datatables.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -123,9 +122,76 @@
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<h4 class="card-title">Adzan Setting</h4>
<hr>
<div class="row">
<div class="col">
<div class="container">
<div class="row py-1">
<div class="col-auto"><label class="col-form-label">Latitude</label></div>
<div class="col-2"><input class="w-100 h-100" type="text" id="adzanlatitude"></div>
<div class="col-auto"><label class="col-form-label">Longitude</label></div>
<div class="col-2"><input class="w-100 h-100" type="text" id="adzanlongitude"></div>
<div class="col-auto"><label class="col-form-label">TimeZone</label></div>
<div class="col"><input class="w-100 h-100" type="text" id="adzantimezone"></div>
</div>
<div class="row">
<p class="py-1" id="adzantoday">Today's Adzan</p>
</div>
<div class="row py-1" id="fajar">
<div class="col-2"><label class="col-form-label w-100 h-100 align-content-center">Fajar</label></div>
<div class="col-2"><input class="w-100 h-100 align-content-center adzantime" id="fajar_time" type="time"></div>
<div class="col d-flex"><input class="flex-grow-1 me-1 adzanfile" type="text"><button class="btn btn-primary col-auto adzanbrowse" type="button">Browse</button></div>
<div class="col-2">
<div class="form-check w-100 h-100 align-content-center"><input class="form-check-input adzanenable" type="checkbox" id="enable_fajr"><label class="form-check-label" for="enable_fajr">Enable</label></div>
</div>
</div>
<div class="row py-1" id="dzuhur">
<div class="col-2"><label class="col-form-label w-100 h-100 align-content-center">Dzuhur</label></div>
<div class="col-2"><input class="w-100 h-100 align-content-center adzantime" id="fajar_time-4" type="time"></div>
<div class="col d-flex"><input class="flex-grow-1 me-1 adzanfile" type="text"><button class="btn btn-primary col-auto adzanbrowse" type="button">Browse</button></div>
<div class="col-2">
<div class="form-check w-100 h-100 align-content-center"><input class="form-check-input adzanenable" type="checkbox" id="enable_fajr-4"><label class="form-check-label" for="enable_fajr-4">Enable</label></div>
</div>
</div>
<div class="row py-1" id="ashar">
<div class="col-2"><label class="col-form-label w-100 h-100 align-content-center">Ashar</label></div>
<div class="col-2"><input class="w-100 h-100 align-content-center adzantime" id="fajar_time-3" type="time"></div>
<div class="col d-flex"><input class="flex-grow-1 me-1 adzanfile" type="text"><button class="btn btn-primary col-auto adzanbrowse" type="button">Browse</button></div>
<div class="col-2">
<div class="form-check w-100 h-100 align-content-center"><input class="form-check-input adzanenable" type="checkbox" id="enable_fajr-3"><label class="form-check-label" for="enable_fajr-3">Enable</label></div>
</div>
</div>
<div class="row py-1" id="magrib">
<div class="col-2"><label class="col-form-label w-100 h-100 align-content-center">Magrib</label></div>
<div class="col-2"><input class="w-100 h-100 align-content-center adzantime" id="fajar_time-2" type="time"></div>
<div class="col d-flex"><input class="flex-grow-1 me-1 adzanfile" type="text"><button class="btn btn-primary col-auto adzanbrowse" type="button">Browse</button></div>
<div class="col-2">
<div class="form-check w-100 h-100 align-content-center"><input class="form-check-input adzanenable" type="checkbox" id="enable_fajr-2"><label class="form-check-label" for="enable_fajr-2">Enable</label></div>
</div>
</div>
<div class="row py-1" id="isya">
<div class="col-2"><label class="col-form-label w-100 h-100 align-content-center">Isya</label></div>
<div class="col-2"><input class="w-100 h-100 align-content-center adzantime" id="fajar_time-1" type="time"></div>
<div class="col d-flex"><input class="flex-grow-1 me-1 adzanfile" type="text"><button class="btn btn-primary col-auto adzanbrowse" type="button">Browse</button></div>
<div class="col-2">
<div class="form-check w-100 h-100 align-content-center"><input class="form-check-input adzanenable" type="checkbox" id="enable_fajr-1"><label class="form-check-label" for="enable_fajr-1">Enable</label></div>
</div>
</div>
</div>
</div>
<div class="col-4 col-sm-3 col-md-2 col-lg-2 col-xl-2"><button class="btn w-100 h-100 pad-button btn-round-basic color-add" id="adzansave" 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/datatables.js"></script>
<script src="assets/js/setting.js"></script>
</body>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -42,8 +41,9 @@
<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 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-import" id="btnExport" type="button">Export</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-import" id="btnImport" type="button">Import</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-import" id="btnFileCheck" type="button">File Check</button></div>
</div>
<div class="row">
<div class="table-responsive">
@@ -120,7 +120,6 @@
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/soundbank.js"></script>
<script src="assets/js/datatables.js"></script>
</body>
</html>

View File

@@ -14,32 +14,82 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.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 streamercard" id="streamercard">
<div class="card-body card-channel">
<h4 class="card-title" id="streamertitle">Channel 01</h4>
<h4 class="card-title streamertitle" 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 class="col-3">
<p class="w-100 h-100 align-content-center">IP</p>
</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 class="col">
<p class="w-100 h-100 align-content-center streamerip" id="streamerip">N/A</p>
</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 class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Buffer</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerbuffer" id="streamerbuffer">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Status</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerstatus" id="streamerstatus">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">File</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerfile" id="streamerfile">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Zones</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerzones" id="streamerzones">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Duration</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerduration" id="streamerduration">N/A</p>
</div>
<div class="col-3">
<p class="w-100 h-100 align-content-center">Elapsed</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerelapsed" id="streamerelapsed">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">VU</p>
</div>
<div class="col">
<div class="progress w-100 h-50 streamervu" id="streamervu">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</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/datatables.js"></script>
</body>
</html>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -25,46 +24,6 @@
<h2 style="text-align: center;">Schedule Bank</h2>
</div>
</div>
<div class="row d-none 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" id="schedulebanktable">
<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">
@@ -106,10 +65,10 @@
</div>
</div>
<div class="row py-2">
<div class="col-7 col-sm-7 col-md-7 col-lg-6 col-xl-6 pad-day">
<div class="col-auto pad-day">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulespecialdate" name="dayselect"><label class="form-check-label" for="schedulespecialdate">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 class="col"><input type="text" id="scheduledate" class="form-control"></div>
</div>
</div>
</div>
@@ -119,13 +78,8 @@
</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 class="row w-100 pad-day">
<div class="col w-100"><input type="text" id="scheduletime" placeholder="HH:mm"></div>
</div>
</div>
</div>
@@ -181,10 +135,78 @@
</div>
</div>
</div>
<div class="accordion" role="tablist" id="accordion-1">
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="false" aria-controls="accordion-1 .item-1">Schedule Table</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-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" id="schedulebanktable">
<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>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="true" aria-controls="accordion-1 .item-2">Today's Schedule</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="table-responsive">
<table class="table" id="todaytable">
<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-1"></tbody>
</table>
</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/schedulebank.js"></script>
<script src="assets/js/datatables.js"></script>
</body>
</html>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -124,7 +123,6 @@
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/tts.js"></script>
</body>

View File

@@ -14,7 +14,6 @@
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
<link rel="stylesheet" href="assets/css/FontAwesome.css">
<link rel="stylesheet" href="assets/css/bss-overrides.css">
<link rel="stylesheet" href="assets/css/datatables.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
@@ -238,7 +237,6 @@
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/bs-init.js"></script>
<script src="assets/js/datatables.js"></script>
<script src="assets/js/usermanagement.js"></script>
</body>

View File

@@ -14,8 +14,16 @@ import commandServer.TCP_Android_Command_Server
import content.Category
import content.Language
import content.VoiceType
import database.Log
import database.data.Log
import database.MariaDB
import database.data.QueueTable
import database.table.Table_Adzan
import database.table.Table_BroadcastZones
import database.table.Table_Logs
import database.table.Table_Messagebank
import database.table.Table_QueuePaging
import database.table.Table_QueueSoundbank
import database.table.Table_SoundChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -29,6 +37,7 @@ import web.WebApp
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.util.TimeZone
import kotlin.concurrent.fixedRateTimer
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
@@ -39,22 +48,21 @@ lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
lateinit var udpreceiver: UDPReceiver
lateinit var tcpreceiver: TCPReceiver
const val version = "0.0.28 (04/02/2026)"
const val version = "0.0.30 (12/02/2026)"
// AAS 64 channels
const val max_channel = 64
val apptick : Long = System.currentTimeMillis()
// 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
)
// 4 tabel utama , kepakai dimana-mana, ditaruh di root biar gampang aksesnya
lateinit var broadcastDB: Table_BroadcastZones
lateinit var soundchannelDB: Table_SoundChannel
lateinit var messageDB: Table_Messagebank
lateinit var queuetableDB: Table_QueueSoundbank
lateinit var queuepagingDB: Table_QueuePaging
lateinit var logDB: Table_Logs
lateinit var adzanTable : Table_Adzan
val contentCache = ContentCache()
/**
* Create necessary folders if not exist
@@ -136,6 +144,12 @@ fun main(args: Array<String>) {
if ("--bypass-dongle" == str.lowercase()){
sdx.BypassDongle = true
}
if (str.startsWith("--default-soundbank=")){
val defaultsb = str.substringAfter("=")
val lang = Language.entries.find { it.value.equals(defaultsb, ignoreCase = true) }
Language.DEFAULT = lang ?: Language.INDONESIA
Logger.info { "Default soundbank language set to ${Language.DEFAULT.value} from command line argument" }
}
}
@@ -169,6 +183,12 @@ fun main(args: Array<String>) {
db = MariaDB()
adzanTable = Table_Adzan(latitude = config.Get(configKeys.LATITUDE.key).toDouble(),
longitude = config.Get(configKeys.LONGITUDE.key).toDouble(),
timezone = TimeZone.getTimeZone(config.Get(configKeys.TIMEZONE.key))
)
adzanTable.GetTodayPrayerTimes()
val subcode01 = MainExtension01()
@@ -176,14 +196,47 @@ fun main(args: Array<String>) {
CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
delay(1000)
adzanTable.CheckTime()?.let{ adz ->
Logger.info { "It's time for Adzan ${adz.prayerName} at ${adz.timeString}" }
val qt = QueueTable(
Source = "AAS",
Type = "ADZAN",
Message = adz.message,
Language = adzanTable.adzan_language,
SB_TAGS = adz.sb_tags,
BroadcastZones = adzanTable.adzan_broadcastzones
)
queuetableDB.Add(qt)
}
// prioritas 1 , habisin queue paging
subcode01.Read_Queue_Paging()
// prioritas 2, habisin queue shalat
subcode01.Read_Queue_Shalat()
if (subcode01.Read_Queue_Paging()){
// processing paging, skip selanjutnya
delay(2000)
continue
}
// // prioritas 2, habisin queue shalat
// if (subcode01.Read_Queue_Shalat()){
// // processing shalat, skip selanjutnya
// delay(2000)
// continue
// }
// prioritas 3, habisin queue timer
subcode01.Read_Queue_Timer()
if (subcode01.Read_Queue_Timer()){
// processing timer, skip selanjutnya
delay(2000)
continue
}
// prioritas 4, habisin queue soundbank
subcode01.Read_Queue_Soundbank()
if (subcode01.Read_Queue_Soundbank()){
// processing soundbank, skip selanjutnya
delay(2000)
continue
}
}
}
@@ -220,14 +273,14 @@ fun main(args: Array<String>) {
val androidserver = TCP_Android_Command_Server()
androidserver.StartTcpServer(5003){
Logger.info { it }
db.logDB.Add(Log.NewLog("ANDROID", it))
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 }
val _sc = soundchannelDB.List.find { it.ip == cmd.ipaddress }
if (_streamer == null) {
// belum create BarixConnection untuk ipaddress ini
@@ -240,6 +293,7 @@ fun main(args: Array<String>) {
_bc.bufferRemain = cmd.buffremain
_bc.statusData = cmd.statusdata
_bc.commandsocket = _tcp
_bc.BarixMode = cmd.isBarix
StreamerOutputs[cmd.ipaddress] = _bc
Logger.info { "Created new Streamer Output for channel ${_sc.channel} with IP ${cmd.ipaddress}" }
@@ -281,13 +335,17 @@ fun main(args: Array<String>) {
}
db.logDB.Add("AAS"," Application started")
adzanTable = Table_Adzan(latitude = config.Get(configKeys.LATITUDE.key).toDouble(),
longitude = config.Get(configKeys.LONGITUDE.key).toDouble(),
timezone = TimeZone.getTimeZone(config.Get(configKeys.TIMEZONE.key))
)
logDB.Add("AAS"," Application started")
// shutdown hook
Runtime.getRuntime().addShutdownHook(Thread ({
db.logDB.Add("AAS"," Application stopping")
logDB.Add("AAS"," Application stopping")
Logger.info { "Shutdown hook called, stopping services..." }
barixserver.StopTcpCommand()
androidserver.StopTcpCommand()

File diff suppressed because it is too large Load Diff

View File

@@ -18,4 +18,39 @@ class AudioFileInfo {
fun isValid() : Boolean {
return fileName.isNotBlank() && fileSize > 0 && duration > 0.0 && bytes.isNotEmpty()
}
/**
* Convert the duration to a human-readable string format.
* @return Duration as a string in HH:MM:SS or MM:SS format or SS if less than a minute.
*/
fun DurationToString() : String {
val totalSeconds = duration.toInt()
val hours = totalSeconds / 3600
val minutes = (totalSeconds % 3600) / 60
val seconds = totalSeconds % 60
return when {
hours > 0 -> String.format("%02d h:%02d m:%02d s", hours, minutes, seconds)
minutes > 0 -> String.format("%02d m:%02d s", minutes, seconds)
else -> String.format("00:%02d s", seconds)
}
}
/**
* Convert the file size to a human-readable string format.
* @return File size as a string in Bytes, KB, MB, or GB.
*/
fun FileSizeToString() : String {
val kb = 1024
val mb = kb * 1024
val gb = mb * 1024
return when {
fileSize >= gb -> String.format("%.2f GB", fileSize.toDouble() / gb)
fileSize >= mb -> String.format("%.2f MB", fileSize.toDouble() / mb)
fileSize >= kb -> String.format("%.2f KB", fileSize.toDouble() / kb)
else -> "$fileSize Bytes"
}
}
}

View File

@@ -1,8 +1,7 @@
package barix
import audio.AudioFileInfo
import audio.Mp3Encoder
import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -14,8 +13,8 @@ import java.net.InetSocketAddress
import java.net.Socket
import java.nio.ByteBuffer
import java.util.function.Consumer
import kotlin.experimental.or
@Suppress("unused")
class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) : AutoCloseable {
private var _bR: Int = 0
private var _sd: Int = 0
@@ -27,6 +26,99 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private val mp3encoder = Mp3Encoder()
private val mp3Consumer = mutableMapOf<String, Consumer<ByteArray>>()
private val udp = DatagramSocket()
private var _barixmode: Boolean = false
private var _usedbybroadcastzone = mutableSetOf<String>()
private var afi : AudioFileInfo? = null
private var starttick: Long? = null
/**
* Set audio file information used for playback
* @param value The AudioFileInfo object containing audio file details
*/
fun SetAudioFileInfo(value: AudioFileInfo?){
afi = value
}
/**
* Get audio file information used for playback
* @return The AudioFileInfo object containing audio file details, or null if not set
*/
fun GetAudioFileInfo() : AudioFileInfo?{
return afi
}
/**
* Set the start tick for playback timing
* @param tick The start tick in milliseconds
*/
fun SetStartTick(tick: Long?){
starttick = tick
}
/**
* Get the elapsed time since the start tick
* @return Elapsed time as a formatted string or "N/A" if start tick is not set
*/
fun GetElapsed() : String {
if (starttick != null){
val elapsedMs = System.currentTimeMillis() - starttick!!
val totalSeconds = elapsedMs / 1000
val hours = totalSeconds / 3600
val minutes = (totalSeconds % 3600) / 60
val seconds = totalSeconds % 60
return when {
hours > 0 -> String.format("%02d h:%02d m:%02d s", hours, minutes, seconds)
minutes > 0 -> String.format("%02d m:%02d s", minutes, seconds)
else -> String.format("00:%02d s", seconds)
}
} else {
return ""
}
}
/**
* Add a broadcast zone that uses this Barix device
* @param zoneName The name of the broadcast zone
*/
fun AddUsedByBroadcastZone(zoneName: String){
_usedbybroadcastzone.add(zoneName)
println("Added used by broadcast zone: $zoneName to Barix device $ipaddress")
}
/**
* Add multiple broadcast zones that use this Barix device
* @param zoneNames The list of broadcast zone names
*/
fun AddUsedByBroadcastZone(zoneNames: List<String>){
for (zoneName in zoneNames){
AddUsedByBroadcastZone(zoneName)
}
}
/**
* Clear all broadcast zones that use this Barix device
*/
fun ClearUsedByBroadcastZones(){
_usedbybroadcastzone.clear()
}
/**
* Get a comma-separated string of broadcast zones that use this Barix device
* @return Comma-separated string of broadcast zones
*/
fun GetUsedByBroadcastZones() : String{
return _usedbybroadcastzone.joinToString(", ")
}
/**
* Barix mode flag
*/
var BarixMode: Boolean
get() = _barixmode
set(value) {
_barixmode = value
}
init {
mp3encoder.Start { data ->
@@ -42,6 +134,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
mp3Consumer.remove(key)
}
@Suppress("unused")
fun Exists_Mp3_Consumer(key: String) : Boolean{
return mp3Consumer.containsKey(key)
}
@@ -118,137 +211,144 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
return statusData == 1
}
/**
* Check if buffer has enough space (more than 10,000 bytes)
* @return true if buffer has enough space
*/
fun bufferEnough(): Boolean{
return isOnline() && (bufferRemain > 10000)
}
/**
* Send data to Barix device via UDP
* @param data The data to send
* @param cbOK Callback function if sending is successful
* @param cbFail Callback function if sending fails
* @param cbPlaying Callback function to indicate if device is playing
*/
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>, cbPlaying: Consumer<Boolean>) {
if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data)
while(!bufferEnough()){
delay(20)
Logger.info{"Waiting for StreamerOutput $ipaddress buffer to have enough space: $bufferRemain bytes available, need more than 10000 bytes"}
}
val bufmax = bufferRemain
val bufkosong = 0.2 * bufmax
val bufpenuh = 0.8 * bufmax
Logger.info{"Starting to send data to StreamerOutput $ipaddress on channel $channel, total data size: ${data.size} bytes, bufferRemain: $bufferRemain bytes, bufkosong: $bufkosong bytes, bufpenuh: $bufpenuh bytes"}
// Ide 07/02/2026, kasih buffer dummy 10x1000 byte pertama biar barix kebacanya stabil
for (i in 1..10){
val chunk = ByteArray(1000){0}
udp.send(DatagramPacket(chunk, chunk.size, inet))
delay(5)
println("Sending dummy buffer $i to $ipaddress")
}
// delay interval awal = 5 ms untuk streamer output dan 10 ms untuk barix
// slow down interval = 5 ms untuk streamer output dan 10 ms untuk barix
// speed up interval = 2 ms untuk streamer output dan 5 ms untuk barix
val slowdowninterval = if (BarixMode) 8L else 5L
val speedupinterval = if (BarixMode) 5L else 2L
var delayinterval = if (BarixMode) 8L else 5L
// buat hitung elapsed
CoroutineScope(Dispatchers.IO).launch {
SetStartTick(System.currentTimeMillis())
cbPlaying.accept(true)
do{
delay(1000)
} while (isPlaying())
cbPlaying.accept(false)
SetStartTick(null)
}
while(bb.hasRemaining()){
try {
val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining())
bb.get(chunk)
while(bufferRemain<chunk.size){
delay(5)
delay(10)
// gas-rem pengiriman
when{
bufferRemain <= bufkosong -> {
if (delayinterval!=slowdowninterval){
delayinterval = slowdowninterval
Logger.info{"Sending to $ipaddress on channel $channel, bufferRemain low: $bufferRemain bytes, slowing down to $delayinterval ms"}
}
}
bufferRemain >= bufpenuh -> {
if (delayinterval!=speedupinterval){
delayinterval = speedupinterval
Logger.info{"Sending to $ipaddress on channel $channel, bufferRemain high: $bufferRemain bytes, speeding up to $delayinterval ms"}
}
}
}
}
udp.send(DatagramPacket(chunk, chunk.size, inet))
mp3encoder.PushData(chunk)
delay(1)
delay(delayinterval)
} catch (e: Exception) {
Logger.error{"SendData to $ipaddress failed, message: ${e.message}"}
cbFail.accept("SendData to $ipaddress failed, message: ${e.message}")
cbPlaying.accept(false)
return@launch
}
}
cbOK.accept("SendData to $channel ($ipaddress) succeeded, ${data.size} bytes sent")
Logger.info{"SendData to $channel ($ipaddress) ended, ${data.size} bytes sent"}
cbOK.accept("SendData to $channel ($ipaddress) ended, ${data.size} bytes sent")
}
} else cbFail.accept("SendData to $ipaddress 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))
if (relays.isNotEmpty()){
var value : Byte = 0
for (r in relays){
if (r in 1..8){
value = value or (1 shl (r - 1)).toByte()
}
}
SendSimpleCommand(byteArrayOf(0x1A, value, 0x61))
}
command.append(binary.toString()).append("@")
SendCommand(command.toString())
}
/**
* Deactivate relay on Barix device
*/
fun DeactivateRelay(){
SendCommand("RELAY;0@")
SendSimpleCommand(byteArrayOf(0x1A, 0, 0x61))
}
/**
* Send command to Barix device
* Send simple command to Barix device
* @param command The command to send
* @return true if successful
*/
fun SendCommand(command: String): Boolean {
fun SendSimpleCommand(command: ByteArray) : 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()
)
if (_tcp !=null){
if (_tcp!!.isConnected){
val out = _tcp!!.getOutputStream()
out.write(b4)
out.write(bb)
out.write(command)
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" }
}
} else throw Exception("Socket to $ipaddress is not connected")
} else throw Exception("Socket to $ipaddress is null")
} catch (e: Exception) {
Logger.error { "Failed to SendCommand to $ipaddress, Message : ${e.message}" }
if (e.message != null && e.message!!.isNotEmpty()) {
Logger.error { "Failed to send command, message : ${e.message}" }
}
return false
}
return false
}
override fun close() {
try{
udp.close()

View File

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

View File

@@ -1,22 +1,21 @@
package barix
import codes.Somecodes.Companion.LitteEndianToInt
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)
//private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?"""
//private val pattern = Regex(regex)
/**
* Start TCP Command Server
@@ -40,33 +39,77 @@ class TCP_Barix_Command_Server {
socketMap[key] = socket
Logger.info { "Start communicating with Streamer Output with IP : $key" }
try{
val din = DataInputStream(socket.getInputStream())
var VuZeroCounter = 0L
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")
val bb = ByteArray(128)
val readbytes = din.read(bb)
if (readbytes == -1) {
Logger.info { "Connection closed by Streamer Output with IP $key" }
break
}
if (readbytes == 0) continue
var stringlength = 0
try{
stringlength = LitteEndianToInt(bb[0], bb[1], bb[2], bb[3])
if (stringlength<1 || stringlength>bb.size-4) throw Exception("Invalid string length $stringlength")
} catch (ex:Exception){
Logger.error { "Error reading length from Streamer Output with IP $key, Message : ${ex.message}" }
continue
}
var str = String(bb,4, stringlength).trim()
if (str.isBlank()) continue
if (!str.startsWith("STATUSBARIX")) continue
if (str.endsWith("@")) str = str.removeSuffix("@")
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" }
// Valid command from StreamerOutput is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain"$
val values = str.split(";")
if (values.size<3) continue
if ("STATUSBARIX" != values[0]) continue
val vu = values[1].toIntOrNull() ?: continue
val buffremain = values[2].toIntOrNull() ?: continue
var status: BarixStatus
when(values.size){
3 ->{
// mode barix
// kadang vu stuck tidak di 0 saat idle,
// jadi kalau vu <512 selama 10 kali berturut2
// dan buffer lebih dari 16000, anggap idle
if ((vu < 512) && (buffremain>=16000)){
VuZeroCounter++
} else {
VuZeroCounter = 0
}
// statusdata = isplaying = , if VuZeroCounter >=10 then idle (0) else playing (1)
val statusdata = if (VuZeroCounter>=10) 0 else 1
status = BarixStatus(
socket.inetAddress.hostAddress,
vu,
buffremain,
statusdata,
true
)
}
4 ->{
// mode Q-AG1
val statusdata = values[3].toIntOrNull() ?: 0
status = BarixStatus(
socket.inetAddress.hostAddress,
vu,
buffremain,
statusdata,
false
)
}
else -> continue
}
cb.accept(status)
}
}

View File

@@ -24,11 +24,13 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.function.BiConsumer
import java.util.function.Consumer
import kotlin.io.path.exists
import kotlin.io.path.name
import kotlin.streams.asSequence
@Suppress("unused")
@@ -109,6 +111,14 @@ class Somecodes {
} else cb.accept(-1,-1)
}
/**
* Convert a string of numbers separated by commas or semicolons into a list of integers.
*/
fun StringToListInt(value : String?) : List<Int>{
if (value.isNullOrBlank()) return listOf()
return value.split(",",";").mapNotNull { it.trim().toIntOrNull() }
}
/**
* Clear all files in PagingResult directory.
* If PagingResult directory does not exist, callback with -1,-1.
@@ -344,6 +354,7 @@ class Somecodes {
// and find them recursively
if (Files.exists(p) && Files.isDirectory(p)){
Files.walk(p)
.asSequence()
// cari file regular saja
.filter { Files.isRegularFile(it)}
// size lebih dari 1KB
@@ -522,6 +533,22 @@ class Somecodes {
return value is String && value.isNotBlank()
}
fun LitteEndianToInt(vararg bb: Byte): Int {
var result = 0
for (i in bb.indices) {
result = result or ((bb[i].toInt() and 0xFF) shl (8 * i))
}
return result
}
fun BigEndianToInt(vararg bb: Byte): Int {
var result = 0
for (i in bb.indices) {
result = result or ((bb[i].toInt() and 0xFF) shl (8 * (bb.size - 1 - i)))
}
return result
}
/**
* Check if all strings in a list are valid non-blank strings.
* @param values The list of strings to check.
@@ -640,6 +667,14 @@ class Somecodes {
}
}
/**
* Get today's date as a string in the format "dd/MM/yyyy".
* @return A string representing today's date.
*/
fun Today_to_DateString() : String {
return dateformat1.format(LocalDateTime.now())
}
/**
* Check if a string is a valid time in the format "hh:mm:ss".
* @param value The string to check.
@@ -675,6 +710,14 @@ class Somecodes {
return false
}
/**
* Get a list of all available time zones.
* @return A list of strings representing the available time zones.
*/
fun Get_TimeZones() : List<String>{
return ZoneId.getAvailableZoneIds().sorted()
}
/**
* Find a schedule day by its name.
* @param value The name of the schedule day to find.
@@ -685,6 +728,21 @@ class Somecodes {
return sd?.name
}
/**
* Check if a string is a valid language code.
* A valid language code is one that matches any of the Language enum values.
* @param value The string to check.
* @return True if the string is a valid language code, false otherwise.
*/
fun ValidLanguage(value: String) : Boolean{
value.split(";",",").forEach { ll ->
Language.entries.forEach { l ->
if (l.value.equals(ll,true)) return true
}
}
return false
}
/**
* 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".
@@ -705,6 +763,56 @@ class Somecodes {
return false
}
/**
* Check if a string is a valid latitude.
* @param value The string to check.
* @return True if the string is a valid latitude, false otherwise.
*/
fun ValidLatitude(value: String) : Boolean {
return try {
val lat = value.toDouble()
lat in -90.0..90.0
} catch (_: Exception) {
false
}
}
fun ValidLatitude(value: Double) : Boolean {
return value in -90.0..90.0
}
/**
* Check if a string is a valid longitude.
* @param value The string to check.
* @return True if the string is a valid longitude, false otherwise.
*/
fun ValidLongitude(value: String) : Boolean {
return try {
val lon = value.toDouble()
lon in -180.0..180.0
} catch (_: Exception) {
false
}
}
fun ValidLongitude(value: Double) : Boolean {
return value in -180.0..180.0
}
/**
* Check if a string is a valid time zone.
* @param value The string to check.
* @return True if the string is a valid time zone, false otherwise.
*/
fun ValidTimeZone(value: String) : Boolean {
return try {
ZoneId.of(value)
true
} catch (_: Exception) {
false
}
}
/**
* Generate a WAV file name with the current date and time.
* The file name format is: [prefix]_ddMMyyyy_HHmmss_[postfix].wav

View File

@@ -1,6 +1,6 @@
package codes
import content.VoiceType
import database.table.AdzanSetting
import org.tinylog.Logger
import java.nio.file.Files
import java.nio.file.Paths
@@ -44,6 +44,44 @@ class configFile {
}
}
class ChangedSetting(
val key: String,
val oldValue: String,
val newValue: String
)
/**
* Compare current Adzan settings with the provided AdzanSetting object.
* @param adz The AdzanSetting object to compare with.
* @return true if some settings are different, false if all are the same.
*/
fun CompareWithAdzanSetting(adz : AdzanSetting) : List<ChangedSetting>{
val changes = mutableListOf<ChangedSetting>()
configKeys.entries.forEach { ck ->
val currentValue = Get(ck.key)
val newValue = when (ck) {
configKeys.ADZAN_FAJR_ENABLED -> adz.fajar_enable.toString()
configKeys.ADZAN_DHUHR_ENABLED -> adz.dzuhur_enable.toString()
configKeys.ADZAN_ASR_ENABLED -> adz.ashar_enable.toString()
configKeys.ADZAN_MAGHRIB_ENABLED -> adz.maghrib_enable.toString()
configKeys.ADZAN_ISHA_ENABLED -> adz.isya_enable.toString()
configKeys.ADZAN_FAJR_SOUND -> adz.fajar_sound
configKeys.ADZAN_DHUHR_SOUND -> adz.dzuhur_sound
configKeys.ADZAN_ASR_SOUND -> adz.ashar_sound
configKeys.ADZAN_MAGHRIB_SOUND -> adz.maghrib_sound
configKeys.ADZAN_ISHA_SOUND -> adz.isya_sound
configKeys.LATITUDE -> adz.latitude.toString()
configKeys.LONGITUDE -> adz.longitude.toString()
configKeys.TIMEZONE -> adz.timezone
else -> return@forEach
}
if (currentValue != newValue){
changes.add(ChangedSetting(key=ck.key, oldValue = currentValue, newValue = newValue))
}
}
return changes
}
private fun HaveAllKeys() : Boolean{
return configKeys.entries.all { config.containsKey(it.key) }
@@ -52,23 +90,9 @@ class configFile {
private fun CreateDefaultConfig(){
config.clear()
// create default config file
config[configKeys.DATABASE_HOST.key] = "localhost"
config[configKeys.DATABASE_PORT.key] = "3306"
config[configKeys.DATABASE_USER.key] = "admin"
config[configKeys.DATABASE_PASSWORD.key] = "admin"
config[configKeys.DATABASE_NAME.key] = "aas"
config[configKeys.SOUNDBANK_DIRECTORY.key] = Paths.get(Somecodes.current_directory, "soundbank").toString()
config[configKeys.REMARK_GOP.key] = ""
config[configKeys.REMARK_GBD.key] = ""
config[configKeys.REMARK_GFC.key] = ""
config[configKeys.REMARK_FLD.key] = ""
config[configKeys.WEBAPP_ADMIN_USERNAME.key] = "admin"
config[configKeys.WEBAPP_ADMIN_PASSWORD.key] = "password"
config[configKeys.WEBAPP_VIEWER_USERNAME.key] = "viewer"
config[configKeys.WEBAPP_VIEWER_PASSWORD.key] = "password"
config[configKeys.WEBAPP_PORT.key] = "3030"
config[configKeys.DEFAULT_VOICE_TYPE.key] = VoiceType.VOICE_1.name
config[configKeys.AUTO_DELETE_RESULT_DAYS.key] = "7"
configKeys.entries.forEach { ck ->
config[ck.key] = ck.defaultValue
}
Save()
}
}

View File

@@ -1,21 +1,42 @@
package codes
enum class configKeys(val key: String) {
DATABASE_HOST("database.host"),
DATABASE_PORT("database.port"),
DATABASE_USER("database.user"),
DATABASE_PASSWORD("database.password"),
DATABASE_NAME("database.name"),
SOUNDBANK_DIRECTORY("soundbank.directory"),
REMARK_GOP("remark.GOP"),
REMARK_GBD("remark.GBD"),
REMARK_GFC("remark.GFC"),
REMARK_FLD("remark.FLD"),
DEFAULT_VOICE_TYPE("default.voice.type"),
WEBAPP_ADMIN_USERNAME("webapp.admin.username"),
WEBAPP_ADMIN_PASSWORD("webapp.admin.password"),
WEBAPP_VIEWER_USERNAME("webapp.viewer.username"),
WEBAPP_VIEWER_PASSWORD("webapp.viewer.password"),
WEBAPP_PORT("webapp.port"),
AUTO_DELETE_RESULT_DAYS("auto.delete.result.days")
import content.VoiceType
import kotlin.io.path.Path
enum class configKeys(val key: String, val defaultValue: String) {
DATABASE_HOST("database.host", "localhost"),
DATABASE_PORT("database.port", "3306"),
DATABASE_USER("database.user", "admin"),
DATABASE_PASSWORD("database.password", "admin"),
DATABASE_NAME("database.name", "aas"),
SOUNDBANK_DIRECTORY("soundbank.directory", Path(Somecodes.current_directory, "soundbank").toString()),
REMARK_GOP("remark.GOP", "GOP"),
REMARK_GBD("remark.GBD", "GBD"),
REMARK_GFC("remark.GFC", "GFC"),
REMARK_FLD("remark.FLD", "FLD"),
DEFAULT_VOICE_TYPE("default.voice.type", VoiceType.VOICE_1.name),
WEBAPP_ADMIN_USERNAME("webapp.admin.username", "admin"),
WEBAPP_ADMIN_PASSWORD("webapp.admin.password", "password"),
WEBAPP_VIEWER_USERNAME("webapp.viewer.username", "viewer"),
WEBAPP_VIEWER_PASSWORD("webapp.viewer.password", "password"),
WEBAPP_PORT("webapp.port", "3030"),
AUTO_DELETE_RESULT_DAYS("auto.delete.result.days", "7"),
LATITUDE("latitude", "-6.1751"),
LONGITUDE("longitude", "106.8272"),
TIMEZONE("timezone", "Asia/Jakarta"),
ADZAN_FAJR_ENABLED("adzan.fajr.enabled", "false"),
ADZAN_DHUHR_ENABLED("adzan.dhuhr.enabled", "false"),
ADZAN_ASR_ENABLED("adzan.asr.enabled", "false"),
ADZAN_MAGHRIB_ENABLED("adzan.maghrib.enabled", "false"),
ADZAN_ISHA_ENABLED("adzan.isha.enabled", "false"),
ADZAN_FAJR_SOUND("adzan.fajr.sound", "adzan_fajr.mp3"),
ADZAN_DHUHR_SOUND("adzan.dhuhr.sound", "adzan_dhuhr.mp3"),
ADZAN_ASR_SOUND("adzan.asr.sound", "adzan_asr.mp3"),
ADZAN_MAGHRIB_SOUND("adzan.maghrib.sound", "adzan_maghrib.mp3"),
ADZAN_ISHA_SOUND("adzan.isha.sound", "adzan_isha.mp3"),
ADZAN_FAJR_MINUTE_OFFSET("adzan.fajr.minute.offset", "0"),
ADZAN_DHUHR_MINUTE_OFFSET("adzan.dhuhr.minute.offset", "0"),
ADZAN_ASR_MINUTE_OFFSET("adzan.asr.minute.offset", "0"),
ADZAN_MAGHRIB_MINUTE_OFFSET("adzan.maghrib.minute.offset", "0"),
ADZAN_ISHA_MINUTE_OFFSET("adzan.isha.minute.offset", "0")
}

View File

@@ -3,12 +3,11 @@ 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 database.data.Messagebank
import database.data.QueuePaging
import database.data.QueueTable
import database.data.Soundbank
import db
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -16,7 +15,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import messageDB
import org.tinylog.Logger
import queuepagingDB
import queuetableDB
import tcpreceiver
import udpreceiver
import java.net.ServerSocket
@@ -72,14 +74,13 @@ class TCP_Android_Command_Server {
//println("Received command from $key : $str")
str.split("@").map { it.trim() }.filter { ValidString(it) }
.forEach {
Logger.info{"Receive command from $key : $it"}
process_command(key,it) { reply ->
try {
val cc = String_to_Byte_Android(reply)
if (cc.isNotEmpty()){
dout.write(cc)
dout.flush()
Logger.info{"Sent reply ${cc.size} bytes to $key : $reply"}
//Logger.info{"Sent reply ${cc.size} bytes to $key : $reply"}
} else Logger.error { "Empty reply to send to $key" }
} catch (e: Exception) {
@@ -149,7 +150,7 @@ class TCP_Android_Command_Server {
* @param cb Callback to send reply string
*/
private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
Logger.info { "Command from $key : $cmd" }
if ("PING" != cmd) Logger.info { "Command from $key : $cmd" }
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }
when (parts[0]) {
"GETLOGIN" -> {
@@ -235,8 +236,8 @@ class TCP_Android_Command_Server {
pj.broadcastzones
)
Logger.info{"Inserting paging audio to queue paging table from Android $key, data=$qp"}
if (db.queuepagingDB.Add(qp)) {
db.queuepagingDB.Resort()
if (queuepagingDB.Add(qp)) {
queuepagingDB.Resort()
logcb.accept("Paging audio inserted to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
cb.accept("PCMFILE_STOP;OK@")
return
@@ -290,8 +291,8 @@ class TCP_Android_Command_Server {
pj.filePath.absolutePathString(),
pj.broadcastzones
)
if (db.queuepagingDB.Add(qp)){
db.queuepagingDB.Resort()
if (queuepagingDB.Add(qp)){
queuepagingDB.Resort()
logcb.accept("Paging audio inserted to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}")
cb.accept("STOPPAGINGAND;OK@")
return
@@ -349,7 +350,7 @@ class TCP_Android_Command_Server {
// iterasi setiap ANN_ID
.forEach { annid ->
// masukin ke VARMESSAGES yang unik secara ANN_ID dan Language
val xx = db.messageDB.List
val xx = messageDB.List
.asSequence()
.filter{it.ANN_ID == annid.toUInt()}
.distinctBy { it.ANN_ID }
@@ -368,11 +369,7 @@ class TCP_Android_Command_Server {
.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)
VARAPTOTAL.addAll(db.soundDB.Find_AirlineName_By_TAG(al))
}
result.append(VARAPTOTAL.size).append("@")
cb.accept(result.toString())
@@ -386,11 +383,7 @@ class TCP_Android_Command_Server {
.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)
VARCITYTOTAL.addAll(db.soundDB.Find_City_By_TAG(ct))
}
result.append(VARCITYTOTAL.size).append("@")
cb.accept(result.toString())
@@ -398,104 +391,56 @@ class TCP_Android_Command_Server {
// 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)
}
val VARPLACESTOTAL = db.soundDB.Get_Places()
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)
}
val VARSHALATTOTAL = db.soundDB.Get_Shalat()
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)
}
val VARSEQUENCETOTAL = db.soundDB.Get_Sequences()
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)
}
val VARREASONTOTAL = db.soundDB.Get_Reasons()
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)
}
val VARPROCEDURETOTAL = db.soundDB.Get_Procedures()
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)
}
val VARGATETOTAL = db.soundDB.Get_Gates()
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)
}
val VARCOMPENSATIONTOTAL = db.soundDB.Get_Compensation()
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)
}
val VARGREETINGTOTAL = db.soundDB.Get_Greeting()
result.append(VARGREETINGTOTAL.size).append("@")
cb.accept(result.toString())
@@ -505,13 +450,13 @@ class TCP_Android_Command_Server {
VARMESSAGES.forEachIndexed { index, msg ->
val ann_id = msg.ANN_ID
val msg_indo = db.messageDB.List.find {
val msg_indo = messageDB.List.find {
it.ANN_ID == ann_id && it.Language.equals(
Language.INDONESIA.name,
true
)
}
val msg_eng = db.messageDB.List.find {
val msg_eng = messageDB.List.find {
it.ANN_ID == ann_id && it.Language.equals(
Language.ENGLISH.name,
true
@@ -636,18 +581,15 @@ class TCP_Android_Command_Server {
if (ValidString(tags)){
if (ValidString(zone)){
val qt = QueueTable(
0u,
LocalDateTime.now().format(datetimeformat1),
"ANDROID",
"SOUNDBANK",
desc,
tags,
zone,
1u,
lang
Source="ANDROID",
Type="SOUNDBANK",
Message=desc,
SB_TAGS = tags,
BroadcastZones = zone,
Language = lang
)
if (db.queuetableDB.Add(qt)){
db.queuetableDB.Resort()
if (queuetableDB.Add(qt)){
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

View File

@@ -5,16 +5,37 @@ package content
*
* @property name The name of the language, as in Soundbank Database
*/
@Suppress("unused")
enum class Language(name: String) {
INDONESIA("INDONESIA"),
ENGLISH("ENGLISH"),
LOCAL("LOCAL"),
JAPANESE("JAPANESE"),
CHINESE("CHINESE"),
ARABIC("ARABIC");
enum class Language(val value: String, val googletts: String) {
INDONESIA(value="INDONESIA", googletts="id-ID"),
ENGLISH(value="ENGLISH", googletts="en-US"),
LOCAL(value="LOCAL", googletts=""),
JAPANESE(value="JAPANESE", googletts="ja-JP"),
CHINESE(value="CHINESE", googletts="zh-CN"),
ARABIC(value="ARABIC", googletts="ar-SA");
companion object{
var DEFAULT: Language = INDONESIA
/**
* Default language link string
*/
fun DefaultLanguageLink() : String {
return DEFAULT.value+";"+ENGLISH.value
}
/**
* Default language order
*/
fun LanguageOrder() : List<String> {
return listOf(
INDONESIA.value,
LOCAL.value,
ENGLISH.value,
CHINESE.value,
JAPANESE.value,
ARABIC.value
)
}
fun from_GoogleTTSLanguage(lang: google.GoogleTTSLanguage) : Language {
return when(lang) {
google.GoogleTTSLanguage.Indonesia -> INDONESIA
@@ -24,15 +45,9 @@ enum class Language(name: String) {
google.GoogleTTSLanguage.Arabic -> ARABIC
}
}
fun from_GoogleTTSLanguage(code: String) : Language {
return when(code) {
"id-ID" -> INDONESIA
"en-US" -> ENGLISH
"ja-JP" -> JAPANESE
"zh-CN" -> CHINESE
"ar-SA" -> ARABIC
else -> INDONESIA
}
fun from_GoogleTTSLanguage(code: String) : Language? {
return entries.find { it.googletts == code }
}
}
}

View File

@@ -1,5 +1,7 @@
package content
import java.time.DayOfWeek
@Suppress("unused")
enum class ScheduleDay(val day: String) {
Sunday("Sunday"),
@@ -9,5 +11,22 @@ enum class ScheduleDay(val day: String) {
Thursday("Thursday"),
Friday("Friday"),
Saturday("Saturday"),
Everyday("Everyday")
}
Everyday("Everyday");
companion object {
/**
* Converts a DayOfWeek to a ScheduleDay
*/
fun from_LocalDate_DOW(value : DayOfWeek) : ScheduleDay{
return when(value){
DayOfWeek.SUNDAY -> Sunday
DayOfWeek.MONDAY -> Monday
DayOfWeek.TUESDAY -> Tuesday
DayOfWeek.WEDNESDAY -> Wednesday
DayOfWeek.THURSDAY -> Thursday
DayOfWeek.FRIDAY -> Friday
DayOfWeek.SATURDAY -> Saturday
}}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,235 +0,0 @@
package database
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.function.Consumer
class Table_Logs(connection: Connection) : dbFunctions<Log> ("logs", connection,listOf("index", "datenya", "timenya", "machine", "description")) {
/**
* dateformat1 is regex for DD/MM/YYYY
*/
val dateformat1 = """^(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[0-2])/\d{4}$""".toRegex()
/**
* dateformat2 is regex for DD-MM-YYYY
*/
val dateformat2 = """^(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-\d{4}$""".toRegex()
/**
* dateformat3 is regex for YYYY/MM/DD
*/
val dateformat3 = """^\d{4}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$""".toRegex()
/**
* dateformat4 is regex for YYYY-MM-DD
*/
val dateformat4 = """^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$""".toRegex()
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"datenya VARCHAR(20) NOT NULL," + // format DD/MM/YYYY
"timenya VARCHAR(20) NOT NULL," + // format HH:MM:SS
"machine VARCHAR(45) NOT NULL," +
"description TEXT NOT NULL" +
")"
super.Create(tabledefinition)
}
fun GetLogForHtml(date: String, filter: String?, cbOK: Consumer<ArrayList<Log>>?, cbFail: Consumer<String>?){
try{
val valid_date : java.sql.Date? = when{
dateformat1.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd/MM/yyyy")))
}
dateformat2.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd-MM-yyyy")))
}
dateformat3.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy/MM/dd")))
}
dateformat4.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
}
else -> null
}
if (valid_date!=null){
// use coalescing for different datenya formats
val statement = if (filter.isNullOrEmpty()){
connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE COALESCE(STR_TO_DATE(datenya,'%d/%m/%Y'), STR_TO_DATE(datenya,'%d-%m-%Y'), STR_TO_DATE(datenya,'%Y/%m/%d'), STR_TO_DATE(datenya,'%Y-%m-%d')) = ?")
} else {
connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE COALESCE(STR_TO_DATE(datenya,'%d/%m/%Y'), STR_TO_DATE(datenya,'%d-%m-%Y'), STR_TO_DATE(datenya,'%Y/%m/%d'), STR_TO_DATE(datenya,'%Y-%m-%d')) = ? AND description LIKE ?")
}
statement?.setDate(1, valid_date)
if (!filter.isNullOrEmpty()){
statement?.setString(2, "%$filter%")
}
val resultSet = statement?.executeQuery()
val tempList = ArrayList<Log>()
while (resultSet?.next() == true) {
val log = Log(
resultSet.getLong("index").toULong(),
resultSet.getString("datenya"),
resultSet.getString("timenya"),
resultSet.getString("machine"),
resultSet.getString("description")
)
tempList.add(log)
}
cbOK?.accept(tempList)
} else throw Exception("Invalid date")
} catch (e : Exception){
if (filter.isNullOrEmpty()){
cbFail?.accept("Failed to Get logs for date $date: ${e.message}")
} else {
cbFail?.accept("Failed to Get logs for date $date with filter $filter: ${e.message}")
}
}
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val log = Log(
resultSet.getLong("index").toULong(),
resultSet.getString("datenya"),
resultSet.getString("timenya"),
resultSet.getString("machine"),
resultSet.getString("description")
)
List.add(log)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
cbFail?.accept("Error fetching ${super.dbName}: ${e.message}")
Logger.error("Error fetching ${super.dbName}: ${e.message}" as Any)
}
}
fun Add(machine: String, description: String): Boolean {
val log = Log.NewLog(machine, description)
return Add(log)
}
override fun Add(data: Log): Boolean {
try {
val statement =
connection.prepareStatement("INSERT INTO logs (datenya, timenya, machine, description) VALUES (?, ?, ?, ?)")
statement?.setString(1, data.datenya)
statement?.setString(2, data.timenya)
statement?.setString(3, data.machine)
statement?.setString(4, data.description)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
return true
} else {
Logger.warn{"Failed to add log entry : $data"}
}
} catch (e: Exception) {
Logger.error{"Error adding log entry: ${e.message}"}
}
return false
}
override fun AddAll(data: ArrayList<Log>): Boolean {
return try {
connection.autoCommit = false
val sql = "INSERT INTO logs (datenya, timenya, machine, description) VALUES (?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (log in data) {
statement.setString(1, log.datenya)
statement.setString(2, log.timenya)
statement.setString(3, log.machine)
statement.setString(4, log.description)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk log insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
true
} catch (e: Exception) {
Logger.error("Error adding log entries: ${e.message}" as Any)
false
}
}
override fun UpdateByIndex(index: Int, data: Log): Boolean {
throw Exception("Update not supported")
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate(
"INSERT INTO $tempdb_name (datenya, timenya, machine, description) " +
"SELECT datenya, timenya, machine, description FROM ${super.dbName} " +
"ORDER BY datenya , timenya , machine "
)
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate(
"INSERT INTO ${super.dbName} (datenya, timenya, machine, description) " +
"SELECT datenya, timenya, machine, description FROM $tempdb_name"
)
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by datenya, timenya, machine" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by datenya, timenya, machine: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
throw Exception("Importing Logs from XLSX is not supported")
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("Log")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "datenya", "timenya", "machine", "description")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("datenya"))
row.createCell(2).setCellValue(resultSet.getString("timenya"))
row.createCell(3).setCellValue(resultSet.getString("machine"))
row.createCell(4).setCellValue(resultSet.getString("description"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting Log, Msg: ${e.message}" }
}
return null
}
}

View File

@@ -0,0 +1,52 @@
package database.data
@Suppress("unused")
data class BroadcastZones(var index: UInt, var description: String, var SoundChannel: String, var id: String, var bp: String){
/**
* Check if all fields are not empty
*/
fun isNotEmpty(): Boolean{
if (description.isNotEmpty()){
if (SoundChannel.isNotEmpty()){
if (id.isNotEmpty()){
if (bp.isNotEmpty()){
return true
}
}
}
}
return false
}
/**
* Return a string representation of the BroadcastZones object.
*/
override fun toString(): String {
return "BroadcastZones(index=$index, description='$description', SoundChannel='$SoundChannel', id='$id', bp='$bp')"
}
companion object{
/**
* Get a list of relay numbers from the broadcast zone's bp field.
* Currently, supports relays 1 to 8.
* @param bz The BroadcastZones object
* @return List of relay numbers (Int) extracted from the bp field
*/
fun getRelaysFromBroadcastZone(bz : BroadcastZones) : List<Int>{
val result = ArrayList<Int>()
// delimiters either comma or semicolon
val parts = bz.bp.split(",", ";")
for (part in parts){
val relay = part.trim().toIntOrNull()
if (relay != null){
if (relay in 1..8){
result.add(relay)
}
}
}
return result
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
data class LanguageLink(var index: UInt, var TAG: String, var Language: String){
@@ -19,4 +19,4 @@ data class LanguageLink(var index: UInt, var TAG: String, var Language: String){
return "LanguageLink(index=$index, TAG='$TAG', Language='$Language')"
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
import java.time.LocalDate
import java.time.LocalTime
@@ -30,4 +30,4 @@ data class Log(
override fun toString() : String {
return "$datenya $timenya [$machine] $description"
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
import java.time.LocalDate
import java.time.LocalTime
@@ -15,4 +15,4 @@ data class LogSemiauto(val index: ULong, val date: String, val time: String, val
return LogSemiauto(0u, date, time, source, description)
}
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
data class Messagebank(
var index : UInt,
@@ -31,4 +31,4 @@ data class Messagebank(
override fun toString(): String {
return "Messagebank(index=$index, Description='$Description', Language='$Language', ANN_ID=$ANN_ID, Voice_Type='$Voice_Type', Message_Detail='$Message_Detail', Message_TAGS='$Message_TAGS')"
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
@Suppress("unused")
data class QueueFids(var index: UInt, var ALCODE: String, var FLNUM: String, var ORIGIN: String, var ETAD: String, var FREMARK: String){

View File

@@ -0,0 +1,33 @@
package database.data
import codes.Somecodes
import java.time.Duration
import java.time.LocalDateTime
data class QueuePaging(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var BroadcastZones: String){
fun isNotEmpty(): Boolean {
return Date_Time.isNotEmpty() && Source.isNotEmpty() && Type.isNotEmpty() && Message.isNotEmpty() && BroadcastZones.isNotEmpty()
}
override fun toString(): String {
return "QueuePaging(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', BroadcastZones='$BroadcastZones')"
}
companion object{
/**
* Check if the QueuePaging entry is expired (older than 5 seconds)
* @param qt QueuePaging entry to check
* @return true if expired, false otherwise
*/
fun isExpired(qt: QueuePaging) : Boolean{
try{
val t1 = LocalDateTime.parse(qt.Date_Time, Somecodes.datetimeformat1)
val delta = Duration.between(t1, LocalDateTime.now())
return delta.seconds > 5
} catch (_: Exception){
return false
}
}
}
}

View File

@@ -0,0 +1,37 @@
package database.data
import codes.Somecodes.Companion.datetimeformat1
import java.time.Duration
import java.time.LocalDateTime
data class QueueTable(var index: UInt=0u, var Date_Time: String =LocalDateTime.now().format(datetimeformat1), var Source: String, var Type: String, var Message: String, var SB_TAGS: String, var BroadcastZones: String, var Repeat: UInt=1u, var Language: String){
/**
* Check if all fields are not empty
*/
fun isNotEmpty(): Boolean {
return Date_Time.isNotEmpty() && Source.isNotEmpty() && Type.isNotEmpty() && Message.isNotEmpty() && SB_TAGS.isNotEmpty() && BroadcastZones.isNotEmpty() && Language.isNotEmpty()
}
override fun toString(): String {
return "QueueTable(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', SB_TAGS='$SB_TAGS', BroadcastZones='$BroadcastZones', Repeat=$Repeat, Language='$Language')"
}
companion object{
/**
* Check if the QueueTable entry is expired (older than 5 seconds)
* @param qt QueueTable entry to check
* @return true if expired, false otherwise
*/
fun isExpired(qt: QueueTable) : Boolean{
try{
val t1 = LocalDateTime.parse(qt.Date_Time, datetimeformat1)
val delta = Duration.between(t1, LocalDateTime.now())
// expired if more than 5 seconds
return delta.seconds > 5
} catch (_: Exception){
return false
}
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
@Suppress("unused")
data class ScheduleBank(
@@ -36,4 +36,4 @@ data class ScheduleBank(
BroadcastZones == other.BroadcastZones &&
Language == other.Language
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
data class SoundChannel(val index: UInt, val channel: String, val ip: String) {
@@ -14,4 +14,4 @@ data class SoundChannel(val index: UInt, val channel: String, val ip: String) {
fun isNotEmpty() : Boolean {
return channel.isNotEmpty() && ip.isNotEmpty()
}
}
}

View File

@@ -1,5 +1,4 @@
package database
package database.data
data class Soundbank(
var index: UInt,
@@ -37,4 +36,4 @@ data class Soundbank(
fun ValidLanguage() : Boolean{
return content.Language.entries.any{ it.name == this.Language}
}
}
}

View File

@@ -1,4 +1,4 @@
package database
package database.data
@Suppress("unused")
data class UserDB(var index: UInt, var username: String, var password: String, var location: String, var airline_tags: String, var city_tags: String, var messagebank_ann_id: String, var broadcastzones: String){
@@ -25,7 +25,4 @@ data class UserDB(var index: UInt, var username: String, var password: String, v
this.broadcastzones == other.broadcastzones
}
}
}

View File

@@ -6,8 +6,30 @@ import java.sql.Connection
import java.util.function.Consumer
@Suppress("unused", "SqlDialectInspection", "SqlSourceToSinkFlow")
abstract class dbFunctions<T>(val dbName: String, val connection: Connection, requiredcolumns: List<String>) {
abstract class dbFunctions<T>(val dbName: String, conn: Connection, requiredcolumns: List<String>) {
var List : ArrayList<T> = ArrayList()
var connection = conn
/**
* Check if the database connection is valid
* @return true if valid, false otherwise
*/
fun IsConnected() : Boolean{
return try {
connection.isValid(2)
} catch (e: Exception) {
Logger.error("Database connection is not valid: ${e.message}" as Any)
false
}
}
/**
* Change the database connection
* @param newcon The new Connection object
*/
fun ChangeConnection(newcon: Connection){
connection = newcon
}
init{
val columns = GetColumnInfo()

View File

@@ -0,0 +1,12 @@
package database.table
/**
* Data class representing Adzan prayer times for a specific date.
* @param date The date for the prayer times in format DD/MM/YYYY.
* @param fajr The Fajr prayer time in format HH:MM.
* @param dhuhr The Dhuhr prayer time in format HH:MM.
* @param asr The Asr prayer time in format HH:MM.
* @param maghrib The Maghrib prayer time in format HH:MM.
* @param isha The Isha prayer time in format HH:MM.
*/
data class AdzanPrayerTime(val date: String, val fajr: String, val dhuhr: String, val asr: String, val maghrib: String, val isha: String)

View File

@@ -0,0 +1,69 @@
package database.table
import codes.Somecodes.Companion.ValidLatitude
import codes.Somecodes.Companion.ValidLongitude
import codes.Somecodes.Companion.ValidScheduleTime
import codes.Somecodes.Companion.ValidTimeZone
import com.fasterxml.jackson.databind.JsonNode
data class AdzanSetting(
val latitude: Double,
val longitude: Double,
val timezone: String,
val fajar_sound: String,
val fajar_enable : Boolean,
val fajar_time : String,
val dzuhur_sound: String,
val dzuhur_enable : Boolean,
val dzuhur_time : String,
val ashar_sound: String,
val ashar_enable : Boolean,
val ashar_time : String,
val maghrib_sound: String,
val maghrib_enable : Boolean,
val maghrib_time : String,
val isya_sound: String,
val isya_enable : Boolean,
val isya_time : String
) {
companion object{
/**
* Create AdzanSetting from JsonNode
* @param json JsonNode object
* @return AdzanSetting object
* @throws Exception if any required field is missing
*/
fun FromJsonNode(json: JsonNode): AdzanSetting{
val xx = AdzanSetting(
latitude = json.get("latitude")?.asDouble() ?: throw Exception("latitude is missing"),
longitude = json.get("longitude")?.asDouble() ?: throw Exception("longitude is missing"),
timezone = json.get("timezone")?.asText() ?: throw Exception("timezone is missing"),
fajar_sound = json.get("fajar_sound")?.asText("") ?: throw Exception("fajar_sound is missing"),
fajar_enable = json.get("fajar_enable")?.asBoolean(false) ?: throw Exception("fajar_enable is missing"),
fajar_time = json.get("fajar_time")?.asText() ?: throw Exception("fajar_time is missing"),
dzuhur_sound = json.get("dzuhur_sound")?.asText() ?: throw Exception("dzuhur_sound is missing"),
dzuhur_enable = json.get("dzuhur_enable")?.asBoolean() ?: throw Exception("dzuhur_enable is missing"),
dzuhur_time = json.get("dzuhur_time")?.asText() ?: throw Exception("dzuhur_time is missing"),
ashar_sound = json.get("ashar_sound")?.asText() ?: throw Exception("ashar_sound is missing"),ashar_enable = json.get("ashar_enable").asBoolean(false),
ashar_time = json.get("ashar_time")?.asText() ?: throw Exception("ashar_time is missing"),
maghrib_sound = json.get("maghrib_sound")?.asText() ?: throw Exception("maghrib_sound is missing"),
maghrib_enable = json.get("maghrib_enable")?.asBoolean() ?: throw Exception("maghrib_enable is missing"),
maghrib_time = json.get("maghrib_time")?.asText() ?: throw Exception("maghrib_time is missing"),
isya_sound = json.get("isya_sound")?.asText() ?: throw Exception("isya_sound is missing"),
isya_enable = json.get("isya_enable")?.asBoolean() ?: throw Exception("isya_enable is missing"),
isya_time = json.get("isya_time")?.asText() ?: throw Exception("isya_time is missing")
)
if (!ValidLatitude(xx.latitude)) throw Exception("Invalid latitude value")
if (!ValidLongitude(xx.longitude)) throw Exception("Invalid longitude value")
if (!ValidTimeZone(xx.timezone)) throw Exception("Invalid timezone value")
if (!ValidScheduleTime(xx.fajar_time)) throw Exception("Invalid fajar_time value")
if (!ValidScheduleTime(xx.dzuhur_time)) throw Exception("Invalid dzuhur_time value")
if (!ValidScheduleTime(xx.ashar_time)) throw Exception("Invalid ashar_time value")
if (!ValidScheduleTime(xx.maghrib_time)) throw Exception("Invalid maghrib_time value")
if (!ValidScheduleTime(xx.isya_time)) throw Exception("Invalid isya_time value")
return xx
}
}
}

View File

@@ -0,0 +1,15 @@
package database.table
enum class AdzanTimeZone {
WIB,
WITA,
WIT;
fun toTimeZoneString(): String {
return when (this) {
WIB -> "Asia/Jakarta"
WITA -> "Asia/Makassar"
WIT -> "Asia/Jayapura"
}
}
}

View File

@@ -0,0 +1,255 @@
package database.table
import codes.Somecodes.Companion.timeformat2
import com.batoulapps.adhan.CalculationMethod
import com.batoulapps.adhan.CalculationParameters
import com.batoulapps.adhan.Coordinates
import com.batoulapps.adhan.Madhab
import com.batoulapps.adhan.PrayerTimes
import com.batoulapps.adhan.data.DateComponents
import content.Language
import messageDB
import org.tinylog.Logger
import java.text.SimpleDateFormat
import java.time.LocalTime
import java.util.Date
import java.util.TimeZone
/**
* Class for calculating prayer times (Adzan) based on latitude, longitude, and time zone.
* @param latitude The latitude of the location. Default is Monas, Jakarta (latitude: -6.1751).
* @param longitude The longitude of the location. Default is Monas, Jakarta (longitude: 106.8272).
* @param timezone The time zone for formatting prayer times. Default is "Asia/Jakarta".
*/
class Table_Adzan(val latitude: Double = -6.1751, val longitude: Double = 106.8272, val timezone : TimeZone = TimeZone.getTimeZone("Asia/Jakarta")) {
var coordinate: Coordinates = Coordinates(latitude, longitude)
val params : CalculationParameters = CalculationMethod.OTHER.parameters
val timeformatter = SimpleDateFormat("HH:mm")
val dateformatter = SimpleDateFormat("dd/MM/yyyy")
var fajar_enable = false
var dzuhur_enable = false
var ashar_enable = false
var maghrib_enable = false
var isya_enable = false
var fajar_message = ""
var dzuhur_message = ""
var ashar_message = ""
var maghrib_message = ""
var isya_message = ""
var fajar_time = ""
var dzuhur_time = ""
var ashar_time = ""
var maghrib_time = ""
var isya_time = ""
var adzan_broadcastzones = ""
var adzan_language = Language.DEFAULT.value
init{
// sumber chatgpt Kemenag
params.fajrAngle = 20.0
params.ishaAngle = 18.0
params.madhab = Madhab.SHAFI
timeformatter.timeZone = timezone
}
data class AdzanTask(
val prayerName : String,
val timeString : String,
val message : String,
val sb_tags : String
)
fun CheckTime() : AdzanTask?{
val hhmm = LocalTime.now().format(timeformat2)
if (hhmm == fajar_time){
if (fajar_enable){
val mb = messageDB.Find_Messagebank(fajar_message, adzan_language)
if (mb!=null){
return AdzanTask(
"Fajar",
fajar_time,
mb.Description,
mb.Message_TAGS
)
} else Logger.error { "Skipped Adzan Fajar because Unable to find $fajar_message in messageDB" }
} else Logger.info{"Skipped Adzan Fajar because fajr_enable is false"}
}
if (hhmm == dzuhur_time){
if (dzuhur_enable){
val mb = messageDB.Find_Messagebank(dzuhur_message, adzan_language)
if (mb!=null){
return AdzanTask(
"Dzuhur",
dzuhur_time,
mb.Description,
mb.Message_TAGS
)
} else Logger.error { "Skipped Adzan Dzuhur because Unable to find $dzuhur_message in messageDB" }
} else Logger.info{"Skipped Adzan Dzuhur because dzuhur_enable is false"}
}
if (hhmm == ashar_time) {
if (ashar_enable){
val mb = messageDB.Find_Messagebank(ashar_message, adzan_language)
if (mb!=null){
return AdzanTask(
"Ashar",
ashar_time,
mb.Description,
mb.Message_TAGS
)
} else Logger.error { "Skipped Adzan Ashar because Unable to find $ashar_message in messageDB" }
} else Logger.info{"Skipped Adzan Ashar because ashar_enable is false"}
}
if (hhmm == maghrib_time){
if (maghrib_enable){
val mb = messageDB.Find_Messagebank(maghrib_message, adzan_language)
if (mb!=null){
return AdzanTask(
"Maghrib",
maghrib_time,
mb.Description,
mb.Message_TAGS
)
} else Logger.error { "Skipped Adzan Maghrib because Unable to find $maghrib_message in messageDB" }
} else Logger.info{"Skipped Adzan Maghrib because maghrib_enable is false"}
}
if (hhmm == isya_time) {
if (isya_enable){
val mb = messageDB.Find_Messagebank(isya_message, adzan_language)
if (mb!=null){
return AdzanTask(
"Isya",
isya_time,
mb.Description,
mb.Message_TAGS
)
} else Logger.error { "Skipped Adzan Isya because Unable to find $isya_message in messageDB" }
} else Logger.info{"Skipped Adzan Isya because isya_enable is false"}
}
return null
}
// /**
// * Change the time zone used for formatting prayer times.
// * @param timezone The new AdzanTimeZone.
// */
// fun ChangeTimezone(timezone: AdzanTimeZone) {
// ChangeTimeZone(timezone.toTimeZoneString())
// }
/**
* Change the time zone used for formatting prayer times.
* @param timezoneString The new time zone string (e.g., "Asia/Jakarta").
*/
fun ChangeTimeZone(timezoneString: String) {
timeformatter.timeZone = TimeZone.getTimeZone(timezoneString)
}
// /**
// * Set prayer time adjustments in minutes.
// * @param fajrMinute Adjustment for Fajr prayer time in minutes.
// * @param dhuhrMinute Adjustment for Dhuhr prayer time in minutes.
// * @param asrMinute Adjustment for Asr prayer time in minutes.
// * @param maghribMinute Adjustment for Maghrib prayer time in minutes.
// * @param ishaMinute Adjustment for Isha prayer time in minutes.
// */
// fun SetPrayerAdjustment(fajrMinute: Int = 0, dhuhrMinute: Int = 0, asrMinute: Int = 0,maghribMinute: Int = 0, ishaMinute: Int = 0) {
// params.adjustments.fajr = fajrMinute
// params.adjustments.dhuhr = dhuhrMinute
// params.adjustments.asr = asrMinute
// params.adjustments.maghrib = maghribMinute
// params.adjustments.isha = ishaMinute
// }
// /**
// * Change the coordinates used for Adzan calculations.
// * @param lat The new latitude.
// * @param long The new longitude.
// */
// fun ChangeCoordinate(lat: Double, long: Double) {
// coordinate = Coordinates(lat, long)
// }
fun ChangeLatitude(lat: Double) {
coordinate = Coordinates(lat, coordinate.longitude)
}
fun ChangeLongitude(long: Double) {
coordinate = Coordinates(coordinate.latitude, long)
}
// /**
// * Get prayer times for a specific date string in the format "dd/MM/yyyy".
// * @param date_string The date string for which to get prayer times.
// * @return An AdzanPrayerTime object containing the prayer times, or null if the date string is invalid.
// */
// fun GetPrayerTimes(date_string: String) : AdzanPrayerTime?{
// try{
// val date = dateformatter.parse(date_string)
// return GetPrayerTimes(date)
// } catch (_: Exception){
// return null
// }
// }
/**
* Get prayer times for a specific date.
* @param date The date for which to get prayer times.
* @return An AdzanPrayerTime object containing the prayer times.
*/
fun GetPrayerTimes(date: Date) : AdzanPrayerTime{
val prayer = PrayerTimes(coordinate, DateComponents.from(date), params)
return AdzanPrayerTime(
dateformatter.format(date),
timeformatter.format(prayer.fajr),
timeformatter.format(prayer.dhuhr),
timeformatter.format(prayer.asr),
timeformatter.format(prayer.maghrib),
timeformatter.format(prayer.isha)
)
}
/**
* Get prayer times for the current date.
* this will update fajar_time, dzuhur_time, ashar_time, maghrib_time, isya_time properties
* @return An AdzanPrayerTime object containing the prayer times for today.
*/
fun GetTodayPrayerTimes() : AdzanPrayerTime{
val result = GetPrayerTimes(Date())
fajar_time = result.fajr
dzuhur_time = result.dhuhr
ashar_time = result.asr
maghrib_time = result.maghrib
isya_time = result.isha
return result
}
// /**
// * Get prayer times for all days in a specific month and year.
// * @param month The month (1-12) for which to get prayer times.
// * @param year The year for which to get prayer times.
// * @return A list of AdzanPrayerTime objects for each day in the specified month and year.
// */
// fun GetMonthlyPrayerTimes(month: Int, year: Int) : List<AdzanPrayerTime>{
// val prayerTimesList = mutableListOf<AdzanPrayerTime>()
// val calendar = java.util.Calendar.getInstance()
// calendar.set(year, month - 1, 1) // Month is 0-based in Calendar
// val daysInMonth = calendar.getActualMaximum(java.util.Calendar.DAY_OF_MONTH)
//
// for (day in 1..daysInMonth) {
// calendar.set(year, month - 1, day)
// val date = calendar.time
// val prayerTimes = GetPrayerTimes(date)
// prayerTimesList.add(prayerTimes)
// }
//
// return prayerTimesList
// }
}

View File

@@ -0,0 +1,332 @@
package database.table
import StreamerOutputs
import codes.Somecodes.Companion.ValidIPV4
import codes.Somecodes.Companion.ValidString
import database.data.BroadcastZones
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import soundchannelDB
import java.sql.Connection
import java.util.function.Consumer
class Table_BroadcastZones(connection: Connection) : dbFunctions<BroadcastZones>("broadcastzones", connection, listOf("index", "description", "SoundChannel", "id", "bp")) {
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"description VARCHAR(512) NOT NULL," + // Description of the broadcast zone
"SoundChannel VARCHAR(45) NOT NULL," + // Sound channel of the broadcast zone
"id VARCHAR(45) NOT NULL," + // Box of the broadcast zone
"bp VARCHAR(45) NOT NULL" + // Relay of the broadcast zone
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val zone = BroadcastZones(
resultSet.getLong("index").toUInt(),
resultSet.getString("description"),
resultSet.getString("SoundChannel"),
resultSet.getString("id"),
resultSet.getString("bp")
)
List.add(zone)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching ${super.dbName} : ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName} : ${e.message}" )
}
}
override fun Add(data: BroadcastZones): Boolean {
try {
val statement =
connection.prepareStatement("INSERT INTO ${super.dbName} (description, SoundChannel, id, bp) VALUES (?, ?, ?, ?)")
statement?.setString(1, data.description)
statement?.setString(2, data.SoundChannel)
statement?.setString(3, data.id)
statement?.setString(4, data.bp)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Broadcast zone added: ${data.description}" as Any)
return true
} else {
Logger.warn("No broadcast zone entry added for: ${data.description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding broadcast zone entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<BroadcastZones>): Boolean {
try {
connection.autoCommit = false
val sql =
"INSERT INTO ${super.dbName} (description, SoundChannel, id, bp) VALUES (?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (bz in data) {
statement.setString(1, bz.description)
statement.setString(2, bz.SoundChannel)
statement.setString(3, bz.id)
statement.setString(4, bz.bp)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk ${super.dbName} insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
return true
} catch (e: Exception) {
Logger.error("Error adding ${super.dbName} entries: ${e.message}" as Any)
}
return false
}
override fun UpdateByIndex(index: Int, data: BroadcastZones): Boolean {
try {
val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET description = ?, SoundChannel = ?, id = ?, bp = ? WHERE `index` = ?")
statement?.setString(1, data.description)
statement?.setString(2, data.SoundChannel)
statement?.setString(3, data.id)
statement?.setString(4, data.bp)
statement?.setLong(5, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Broadcast zone updated at index $index: ${data.description}" as Any)
return true
} else {
Logger.warn("No broadcast zone entry updated at index $index for: ${data.description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating broadcast zone entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (description, SoundChannel, id, bp) SELECT description, SoundChannel, id, bp FROM ${super.dbName} ORDER BY description ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (description, SoundChannel, id, bp) SELECT description, SoundChannel, id, bp FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by description" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by description: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
val sheet = workbook.getSheet("BroadcastZones")
?: throw Exception("No sheet named 'BroadcastZones' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf("Index", "description", "SoundChannel", "id", "bp")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing broadcast_zones
Clear()
// read each row and insert into database
val _broadcastZonesList = ArrayList<BroadcastZones>()
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val description = row.getCell(1)?.stringCellValue ?: continue
val soundChannel = row.getCell(2)?.stringCellValue ?: continue
val id = row.getCell(3)?.stringCellValue ?: continue
val bp = row.getCell(4)?.stringCellValue ?: continue
val broadcastZone = BroadcastZones(0u, description, soundChannel, id, bp)
_broadcastZonesList.add(broadcastZone)
}
return AddAll(_broadcastZonesList)
} catch (e: Exception) {
Logger.error { "Error importing BroadcastZones, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("BroadcastZones")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "description", "SoundChannel", "id", "bp")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("description"))
row.createCell(2).setCellValue(resultSet.getString("SoundChannel"))
row.createCell(3).setCellValue(resultSet.getString("id"))
row.createCell(4).setCellValue(resultSet.getString("bp"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting BroadcastZones, Msg: ${e.message}" }
}
return null
}
/**
* Get all distinct broadcast zone descriptions from broadcastDB
* @return a list of distinct broadcast zone descriptions sorted alphabetically
*/
fun Get_BroadcastZone_List(): List<String> {
return List
.distinctBy { it.description }
.map { it.description }
.sorted()
}
data class InvalidZoneDetail(val zonename: String, val reason: String)
data class ValidZoneDetail(
val zonename: String,
val soundchanel: String,
val ip: String,
val boxid: String,
val contacts: String
)
class CheckBroadcastZoneResult {
var allvalid: Boolean = false
var message: String? = null
var validzones = mutableListOf<ValidZoneDetail>()
var invalidzones = mutableListOf<InvalidZoneDetail>()
}
/**
* Check if all broadcast zones in a comma-separated string are valid,
* means the broadcast zone name exists in the BroadcastZones table, its SoundChannel exists in the SoundChannel table, and its IP is valid
* @param zones Comma-separated string of broadcast zones
* @param checkOnline Whether to check if the sound channel is really online, recorded in StreamerOutputs map
* @return true if all broadcast zones are valid, false otherwise
*/
fun AllBroadcastZonesValid(zones: String, checkOnline: Boolean = true) : Boolean{
val bzlist = zones.split(",",";").map { it.trim() }.filter { it.isNotEmpty() }
//println("Checking all broadcast zones validity for: $bzlist")
AllBroadcastZonesValid(bzlist, checkOnline).let { result ->
if (result.allvalid) {
//Logger.info("All broadcast zones are valid: ${bzlist.joinToString(", ")}" as Any)
return true
} else {
//Logger.warn("Some broadcast zones are invalid:" as Any)
result.invalidzones.forEach { iz ->
Logger.warn(" - Zone '${iz.zonename}' is invalid: ${iz.reason}" as Any)
}
return false
}
}
//return AllBroadcastZonesValid(bzlist).allvalid
}
/**
* Fungsi untuk cek apakah semua broadcast zone valid
* Valid berarti nama broadcast zone ada di tabel BroadcastZones, dan SoundChannel-nya ada di tabel SoundChannel, dan IP-nya valid
* @param bz List of broadcast zone (SoundChannel)
* @param checkOnline Whether to check if the sound channel is really online, recorded in StreamerOutputs map
* @return CheckBroadcastZoneResult object containing allvalid flag and list of invalid zones
*/
fun AllBroadcastZonesValid(bz: List<String>, checkOnline: Boolean = true): CheckBroadcastZoneResult {
val result = CheckBroadcastZoneResult()
if (bz.isNotEmpty()) {
bz.forEach { zz ->
if (ValidString(zz)) { // string tidak kosong
val findzone = List.find{
ValidString(it.description) && ValidString(it.SoundChannel) && it.description.equals(zz,true)
}
if (findzone != null) { // ketemu zona dengan deskripsi sesuai
val findsc = soundchannelDB.List.find {
findzone.SoundChannel.equals(
it.channel,
true
) && ValidIPV4(it.ip)
}
if (findsc != null) { // ketemu soundchannel dengan channel sesuai dan IP valid
// check apakah offline atau online
if (checkOnline){
if (StreamerOutputs.containsKey(findsc.ip)) {
val bc = StreamerOutputs[findsc.ip]
if (bc != null && bc.isOnline()) {
result.validzones.add(
ValidZoneDetail(
zz,
findzone.SoundChannel,
findsc.ip,
findzone.id,
findzone.bp
)
)
} else result.invalidzones.add(
InvalidZoneDetail(
zz,
"SoundChannel ${findzone.SoundChannel} with IP ${findsc.ip} is offline"
)
)
} else result.invalidzones.add(
InvalidZoneDetail(
zz,
"SoundChannel ${findzone.SoundChannel} with IP ${findsc.ip} is not connected in StreamerOutputs"
)
)
} else {
result.validzones.add(
ValidZoneDetail(
zz,
findzone.SoundChannel,
findsc.ip,
findzone.id,
findzone.bp
)
)
}
} else result.invalidzones.add(
InvalidZoneDetail(
zz,
"SoundChannel ${findzone.SoundChannel} not found or has invalid IP in SoundChannel table"
)
)
} else result.invalidzones.add(InvalidZoneDetail(zz, "Zone $zz not found in BroadcastZones table"))
} else result.invalidzones.add(InvalidZoneDetail(zz, "Invalid broadcast zone string"))
}
if (result.validzones.size == bz.size) {
result.allvalid = true
result.message = "All requested broadcast zones are valid"
} else {
result.message = "Some requested broadcast zones are not registered in BroadcastZone table"
}
} else {
result.message = "No Broadcast Zones checked for validity"
}
return result
}
}

View File

@@ -0,0 +1,180 @@
package database.table
import database.data.LanguageLink
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
class Table_LanguageLink(connection: Connection) : dbFunctions<LanguageLink>("languagelinking", connection, listOf("index", "TAG", "Language")) {
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"TAG VARCHAR(45) NOT NULL," + // Language tag (e.g., EN, FR)
"Language VARCHAR(128) NOT NULL" + // Full language name (e.g., English, French)
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val languageLink = LanguageLink(
resultSet.getLong("index").toUInt(),
resultSet.getString("TAG"),
resultSet.getString("Language")
)
List.add(languageLink)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching ${super.dbName} : ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName} : ${e.message}" )
}
}
override fun Add(data: LanguageLink): Boolean {
try {
val statement = connection.prepareStatement("INSERT INTO ${super.dbName} (TAG, Language) VALUES (?, ?)")
statement.setString(1, data.TAG)
statement.setString(2, data.Language)
val rowsAffected = statement.executeUpdate()
if (rowsAffected > 0) {
Logger.info("Language link added: ${data.TAG} -> ${data.Language}" as Any)
return true
} else {
Logger.warn("No language link entry added for: ${data.TAG} -> ${data.Language}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding language link entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<LanguageLink>): Boolean {
try {
connection.autoCommit = false
val sql = "INSERT INTO ${super.dbName} (TAG, Language) VALUES (?, ?)"
val statement = connection.prepareStatement(sql)
//for (ll in List) {
for (ll in data) {
statement.setString(1, ll.TAG)
statement.setString(2, ll.Language)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk languagelinking insert successful: ${List.size} entries" as Any)
connection.autoCommit = true
return true
} catch (e: Exception) {
Logger.error("Error adding languagelinking entries: ${e.message}" as Any)
}
return false
}
override fun UpdateByIndex(index: Int, data: LanguageLink): Boolean {
try {
val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET TAG = ?, Language = ? WHERE `index` = ?")
statement?.setString(1, data.TAG)
statement?.setString(2, data.Language)
statement?.setLong(3, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Language link updated at index $index: ${data.TAG} -> ${data.Language}" as Any)
return true
} else {
Logger.warn("No language link entry updated at index $index for: ${data.TAG} -> ${data.Language}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating language link entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (TAG, Language) SELECT TAG, Language FROM ${super.dbName} ORDER BY TAG")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (TAG, Language) SELECT TAG, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by TAG" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by TAG: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
val sheet =
workbook.getSheet("LanguageLink") ?: throw Exception("No sheet named 'LanguageLink' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf("Index", "TAG", "Language")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing languagelink
Clear()
// read each row and insert into database
val _languageLinkList = ArrayList<LanguageLink>()
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val tag = row.getCell(1)?.stringCellValue ?: continue
val language = row.getCell(2)?.stringCellValue ?: continue
val languageLink = LanguageLink(0u, tag, language)
_languageLinkList.add(languageLink)
}
return AddAll(_languageLinkList)
} catch (e: Exception) {
Logger.error { "Error importing LanguageLink, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("LanguageLink")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "TAG", "Language")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("TAG"))
row.createCell(2).setCellValue(resultSet.getString("Language"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting languagelinking, Msg: ${e.message}" }
}
return null
}
}

View File

@@ -1,8 +1,11 @@
package database
package database.table
import database.data.LogSemiauto
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.sql.Date
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.function.Consumer
@@ -44,26 +47,27 @@ class Table_LogSemiAuto(connection: Connection) : dbFunctions<LogSemiauto>("logs
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val log = LogSemiauto(
resultSet.getLong("index").toULong(),
resultSet.getString("date"),
resultSet.getString("time"),
resultSet.getString("source"),
resultSet.getString("description")
)
List.add(log)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
cbFail?.accept("Error fetching ${super.dbName}: ${e.message}")
Logger.error("Error fetching ${super.dbName}: ${e.message}" as Any)
}
throw Exception("Getting all LogSemiauto entries is not supported")
// List.clear()
// try {
// val statement = connection.createStatement()
// val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
// while (resultSet?.next() == true) {
// val log = LogSemiauto(
// resultSet.getLong("index").toULong(),
// resultSet.getString("date"),
// resultSet.getString("time"),
// resultSet.getString("source"),
// resultSet.getString("description")
// )
// List.add(log)
// }
// cbOK?.accept(Unit)
// } catch (e: Exception) {
// cbFail?.accept("Error fetching ${super.dbName}: ${e.message}")
// Logger.error("Error fetching ${super.dbName}: ${e.message}" as Any)
//
// }
}
fun Add(source: String, description: String){
@@ -92,18 +96,18 @@ class Table_LogSemiAuto(connection: Connection) : dbFunctions<LogSemiauto>("logs
fun GetLogSemiAutoForHtml(date: String, filter: String?, cbOK: Consumer<ArrayList<LogSemiauto>>?, cbFail: Consumer<String>?){
try{
val valid_date : java.sql.Date? = when{
val valid_date : Date? = when{
dateformat1.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd/MM/yyyy")))
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd/MM/yyyy")))
}
dateformat2.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd-MM-yyyy")))
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd-MM-yyyy")))
}
dateformat3.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy/MM/dd")))
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy/MM/dd")))
}
dateformat4.matches(date) -> {
java.sql.Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
}
else -> null
}
@@ -179,31 +183,32 @@ class Table_LogSemiAuto(connection: Connection) : dbFunctions<LogSemiauto>("logs
}
override fun Resort(): Boolean {
try{
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate(
"INSERT INTO $tempdb_name (date, time, source, description) " +
"SELECT date, time, source, description FROM ${super.dbName} " +
"ORDER BY date , time , source "
)
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate(
"INSERT INTO ${super.dbName} (date, time, source, description) " +
"SELECT date, time, source, description FROM $tempdb_name"
)
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by date, time, source" as Any)
// reload the local list
Get()
return true
} catch (e : Exception){
Logger.error { "Error resorting logsemiauto table: ${e.message}" }
return false
}
throw Exception("Resorting LogSemiauto table is not supported")
// try{
// val statement = connection.createStatement()
// val tempdb_name = "temp_${super.dbName}"
// // use a temporary table to reorder the index
// statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
// statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
// statement?.executeUpdate(
// "INSERT INTO $tempdb_name (date, time, source, description) " +
// "SELECT date, time, source, description FROM ${super.dbName} " +
// "ORDER BY date , time , source "
// )
// statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
// statement?.executeUpdate(
// "INSERT INTO ${super.dbName} (date, time, source, description) " +
// "SELECT date, time, source, description FROM $tempdb_name"
// )
// statement?.executeUpdate("DROP TABLE $tempdb_name")
// Logger.info("${super.dbName} table resorted by date, time, source" as Any)
// // reload the local list
// Get()
// return true
// } catch (e : Exception){
// Logger.error { "Error resorting logsemiauto table: ${e.message}" }
// return false
// }
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {

View File

@@ -0,0 +1,327 @@
package database.table
import database.data.Log
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.sql.Date
import java.sql.Statement
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.function.Consumer
class Table_Logs(connection: Connection) : dbFunctions<Log>("logs", connection,listOf("index", "datenya", "timenya", "machine", "description")) {
/**
* dateformat1 is regex for DD/MM/YYYY
*/
val dateformat1 = """^(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[0-2])/\d{4}$""".toRegex()
/**
* dateformat2 is regex for DD-MM-YYYY
*/
val dateformat2 = """^(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-\d{4}$""".toRegex()
/**
* dateformat3 is regex for YYYY/MM/DD
*/
val dateformat3 = """^\d{4}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$""".toRegex()
/**
* dateformat4 is regex for YYYY-MM-DD
*/
val dateformat4 = """^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$""".toRegex()
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"datenya VARCHAR(20) NOT NULL," + // format DD/MM/YYYY
"timenya VARCHAR(20) NOT NULL," + // format HH:MM:SS
"machine VARCHAR(45) NOT NULL," +
"description TEXT NOT NULL" +
")"
super.Create(tabledefinition)
}
fun GetLogForHtml(date: String, filter: String?, cbOK: Consumer<ArrayList<Log>>?, cbFail: Consumer<String>?){
try{
var statement : Statement?
if ("alldate" == date){
if (filter.isNullOrEmpty()) throw Exception("Filter is required when date is 'alldate'")
statement = connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE description LIKE ?")
statement?.setString(1, "%$filter%")
} else {
val valid_date : Date? = when{
dateformat1.matches(date) -> {
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd/MM/yyyy")))
}
dateformat2.matches(date) -> {
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("dd-MM-yyyy")))
}
dateformat3.matches(date) -> {
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy/MM/dd")))
}
dateformat4.matches(date) -> {
Date.valueOf(LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
}
else -> null
}
if (valid_date!=null){
// use coalescing for different datenya formats
statement = if (filter.isNullOrEmpty()){
connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE COALESCE(STR_TO_DATE(datenya,'%d/%m/%Y'), STR_TO_DATE(datenya,'%d-%m-%Y'), STR_TO_DATE(datenya,'%Y/%m/%d'), STR_TO_DATE(datenya,'%Y-%m-%d')) = ?")
} else {
connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE COALESCE(STR_TO_DATE(datenya,'%d/%m/%Y'), STR_TO_DATE(datenya,'%d-%m-%Y'), STR_TO_DATE(datenya,'%Y/%m/%d'), STR_TO_DATE(datenya,'%Y-%m-%d')) = ? AND description LIKE ?")
}
statement?.setDate(1, valid_date)
if (!filter.isNullOrEmpty()){
statement?.setString(2, "%$filter%")
}
} else throw Exception("Invalid date")
}
if (statement!=null){
val resultSet = statement.executeQuery()
val tempList = ArrayList<Log>()
while (resultSet?.next() == true) {
val log = Log(
resultSet.getLong("index").toULong(),
resultSet.getString("datenya"),
resultSet.getString("timenya"),
resultSet.getString("machine"),
resultSet.getString("description")
)
tempList.add(log)
}
cbOK?.accept(tempList)
} else throw Exception("Failed to prepare statement")
} catch (e : Exception){
if (filter.isNullOrEmpty()){
cbFail?.accept("Failed to Get logs for date $date: ${e.message}")
} else {
cbFail?.accept("Failed to Get logs for date $date with filter $filter: ${e.message}")
}
}
}
/****
* Fetches all log entries from the database and populates the local List.
* @param cbOK Optional callback invoked upon successful retrieval.
* @param cbFail Optional callback invoked upon failure with an error message.
*/
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
throw Exception("Get all logs is not supported for Logs")
// CoroutineScope(Dispatchers.IO).launch {
// List.clear()
// try {
// val statement = connection.createStatement()
// val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
// while (resultSet?.next() == true) {
// val log = Log(
// resultSet.getLong("index").toULong(),
// resultSet.getString("datenya"),
// resultSet.getString("timenya"),
// resultSet.getString("machine"),
// resultSet.getString("description")
// )
// List.add(log)
// }
// cbOK?.accept(Unit)
// } catch (e: Exception) {
// cbFail?.accept("Error fetching ${super.dbName}: ${e.message}")
// Logger.error("Error fetching ${super.dbName}: ${e.message}" as Any)
//
// }
// }
}
fun Add(machine: String, description: String): Boolean {
val log = Log.NewLog(machine, description)
return Add(log)
}
override fun Add(data: Log): Boolean {
try {
val statement =
connection.prepareStatement("INSERT INTO logs (datenya, timenya, machine, description) VALUES (?, ?, ?, ?)")
statement?.setString(1, data.datenya)
statement?.setString(2, data.timenya)
statement?.setString(3, data.machine)
statement?.setString(4, data.description)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
return true
} else {
Logger.warn{"Failed to add log entry : $data"}
}
} catch (e: Exception) {
Logger.error{"Error adding log entry: ${e.message}"}
}
return false
}
override fun AddAll(data: ArrayList<Log>): Boolean {
return try {
connection.autoCommit = false
val sql = "INSERT INTO logs (datenya, timenya, machine, description) VALUES (?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (log in data) {
statement.setString(1, log.datenya)
statement.setString(2, log.timenya)
statement.setString(3, log.machine)
statement.setString(4, log.description)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk log insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
true
} catch (e: Exception) {
Logger.error("Error adding log entries: ${e.message}" as Any)
false
}
}
override fun UpdateByIndex(index: Int, data: Log): Boolean {
throw Exception("Update not supported")
}
override fun Resort(): Boolean {
throw Exception("Resorting Logs is not supported")
// try {
// val statement = connection.createStatement()
// val tempdb_name = "temp_${super.dbName}"
// // use a temporary table to reorder the index
// statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
// statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
// statement?.executeUpdate(
// "INSERT INTO $tempdb_name (datenya, timenya, machine, description) " +
// "SELECT datenya, timenya, machine, description FROM ${super.dbName} " +
// "ORDER BY datenya , timenya , machine "
// )
// statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
// statement?.executeUpdate(
// "INSERT INTO ${super.dbName} (datenya, timenya, machine, description) " +
// "SELECT datenya, timenya, machine, description FROM $tempdb_name"
// )
// statement?.executeUpdate("DROP TABLE $tempdb_name")
// Logger.info("${super.dbName} table resorted by datenya, timenya, machine" as Any)
// // reload the local list
// Get()
// return true
// } catch (e: Exception) {
// Logger.error("Error resorting ${super.dbName} table by datenya, timenya, machine: ${e.message}" as Any)
// }
// return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
throw Exception("Importing Logs from XLSX is not supported")
}
/**
* Exports the log table to an XLSX workbook for a specific date and optional filter.
* @param logDate The date string in format "dd/MM/yyyy".
* @param logFilter The filter string for the description or machine. If empty, exports all logs for the date.
* @return An XSSFWorkbook containing the filtered log data, or null if an error occurred.
*/
fun Export_Log_XLSX(logDate: String, logFilter: String): XSSFWorkbook? {
try {
val valid_date : Date? = when{
dateformat1.matches(logDate) -> {
Date.valueOf(LocalDate.parse(logDate, DateTimeFormatter.ofPattern("dd/MM/yyyy")))
}
dateformat2.matches(logDate) -> {
Date.valueOf(LocalDate.parse(logDate, DateTimeFormatter.ofPattern("dd-MM-yyyy")))
}
dateformat3.matches(logDate) -> {
Date.valueOf(LocalDate.parse(logDate, DateTimeFormatter.ofPattern("yyyy/MM/dd")))
}
dateformat4.matches(logDate) -> {
Date.valueOf(LocalDate.parse(logDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
}
else -> null
}
if (valid_date!=null){
// use coalescing for different datenya formats
val statement = if (logFilter.isEmpty()){
connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE COALESCE(STR_TO_DATE(datenya,'%d/%m/%Y'), STR_TO_DATE(datenya,'%d-%m-%Y'), STR_TO_DATE(datenya,'%Y/%m/%d'), STR_TO_DATE(datenya,'%Y-%m-%d')) = ?")
} else {
connection.prepareStatement("SELECT * FROM ${super.dbName} WHERE COALESCE(STR_TO_DATE(datenya,'%d/%m/%Y'), STR_TO_DATE(datenya,'%d-%m-%Y'), STR_TO_DATE(datenya,'%Y/%m/%d'), STR_TO_DATE(datenya,'%Y-%m-%d')) = ? AND description LIKE ?")
}
statement?.setDate(1, valid_date)
if (logFilter.isNotEmpty()){
statement?.setString(2, "%$logFilter%")
}
val resultSet = statement?.executeQuery()
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("Log")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "datenya", "timenya", "machine", "description")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("datenya"))
row.createCell(2).setCellValue(resultSet.getString("timenya"))
row.createCell(3).setCellValue(resultSet.getString("machine"))
row.createCell(4).setCellValue(resultSet.getString("description"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} else throw Exception("Invalid date")
} catch (e: Exception) {
Logger.error { "Error exporting Log, Msg: ${e.message}" }
}
return null
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("Log")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "datenya", "timenya", "machine", "description")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("datenya"))
row.createCell(2).setCellValue(resultSet.getString("timenya"))
row.createCell(3).setCellValue(resultSet.getString("machine"))
row.createCell(4).setCellValue(resultSet.getString("description"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting Log, Msg: ${e.message}" }
}
return null
}
}

View File

@@ -0,0 +1,282 @@
package database.table
import database.data.Messagebank
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
class Table_Messagebank(connection: Connection) : dbFunctions<Messagebank>("messagebank", connection, listOf("index", "Description", "Language", "ANN_ID", "Voice_Type", "Message_Detail", "Message_TAGS")) {
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"Description VARCHAR(512) NOT NULL," + // Description of the message
"Language VARCHAR(45) NOT NULL," + // Language of the message
"ANN_ID INT NOT NULL," + // ANN ID of the message
"Voice_Type VARCHAR(45) NOT NULL," + // Voice type of the message
"Message_Detail VARCHAR(1024) NOT NULL," + // Full message text
"Message_TAGS VARCHAR(1024)" + // Comma-separated tags for the message
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val messagebank = Messagebank(
resultSet.getLong("index").toUInt(),
resultSet.getString("Description"),
resultSet.getString("Language"),
resultSet.getInt("ANN_ID").toUInt(),
resultSet.getString("Voice_Type"),
resultSet.getString("Message_Detail"),
resultSet.getString("Message_TAGS")
)
List.add(messagebank)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching ${super.dbName} : ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName} : ${e.message}")
}
}
override fun Add(data: Messagebank): Boolean {
try {
val statement =
connection.prepareStatement("INSERT INTO ${super.dbName} (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) VALUES (?, ?, ?, ?, ?, ?)")
statement?.setString(1, data.Description)
statement?.setString(2, data.Language)
statement?.setInt(3, data.ANN_ID.toInt())
statement?.setString(4, data.Voice_Type)
statement?.setString(5, data.Message_Detail)
statement?.setString(6, data.Message_TAGS)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Messagebank added: ${data.Description}" as Any)
return true
} else {
Logger.warn("No messagebank entry added for: ${data.Description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding messagebank entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<Messagebank>): Boolean {
try {
connection.autoCommit = false
val sql =
"INSERT INTO ${super.dbName} (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) VALUES (?, ?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (mb in data) {
statement.setString(1, mb.Description)
statement.setString(2, mb.Language)
statement.setInt(3, mb.ANN_ID.toInt())
statement.setString(4, mb.Voice_Type)
statement.setString(5, mb.Message_Detail)
statement.setString(6, mb.Message_TAGS)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk messagebank insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
return true
} catch (e: Exception) {
Logger.error("Error adding messagebank entries: ${e.message}" as Any)
}
return false
}
override fun UpdateByIndex(index: Int, data: Messagebank): Boolean {
try {
val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET Description = ?, Language = ?, ANN_ID = ?, Voice_Type = ?, Message_Detail = ?, Message_TAGS = ? WHERE `index` = ?")
statement?.setString(1, data.Description)
statement?.setString(2, data.Language)
statement?.setInt(3, data.ANN_ID.toInt())
statement?.setString(4, data.Voice_Type)
statement?.setString(5, data.Message_Detail)
statement?.setString(6, data.Message_TAGS)
statement?.setLong(7, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Messagebank updated at index $index: ${data.Description}" as Any)
return true
} else {
Logger.warn("No messagebank entry updated at index $index for: ${data.Description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating messagebank entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) SELECT Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS FROM ${super.dbName} ORDER BY ANN_ID ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS) SELECT Description, Language, ANN_ID, Voice_Type, Message_Detail, Message_TAGS FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by Description" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by Description: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
// check if there is sheet named "Messagebank"
val sheet =
workbook.getSheet("Messagebank") ?: throw Exception("No sheet named 'Messagebank' found")
// check if the sheet contains header named "Index", "Description", "Language", "ANN_ID", "Voice_Type", "Message_Detail", "Message_TAGS"
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers =
arrayOf(
"Index",
"Description",
"Language",
"ANN_ID",
"Voice_Type",
"Message_Detail",
"Message_TAGS"
)
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing messagebank
Clear()
// read each row and insert into database
val _messagebankList = ArrayList<Messagebank>()
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val description = row.getCell(1)?.stringCellValue ?: continue
val language = row.getCell(2)?.stringCellValue ?: continue
val annId = row.getCell(3)?.stringCellValue?.toUIntOrNull() ?: continue
val voiceType = row.getCell(4)?.stringCellValue ?: continue
val messageDetail = row.getCell(5)?.stringCellValue ?: continue
val messageTags = row.getCell(6)?.stringCellValue ?: continue
val messagebank =
Messagebank(0u, description, language, annId, voiceType, messageDetail, messageTags)
_messagebankList.add(messagebank)
}
return AddAll(_messagebankList)
} catch (e: Exception) {
Logger.error { "Error importing Messagebank, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("Messagebank")
val headerRow = sheet.createRow(0)
val headers =
arrayOf(
"Index",
"Description",
"Language",
"ANN_ID",
"Voice_Type",
"Message_Detail",
"Message_TAGS"
)
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("Description"))
row.createCell(2).setCellValue(resultSet.getString("Language"))
row.createCell(3).setCellValue(resultSet.getString("ANN_ID"))
row.createCell(4).setCellValue(resultSet.getString("Voice_Type"))
row.createCell(5).setCellValue(resultSet.getString("Message_Detail"))
row.createCell(6).setCellValue(resultSet.getString("Message_TAGS"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting Messagebank, Msg: ${e.message}" }
}
return null
}
/**
* Get all distinct message ID from messagebank
* @return a list of distinct ANN_ID sorted numerically
*/
fun Get_MessageID_List(): List<UInt> {
return List
.distinctBy { it.ANN_ID }
.map { it.ANN_ID }
.sorted()
}
// valid messagedetail is message_name [ann_id]
// so we need regex to check if messagedetail matches that format
private val messageDetailRegex = """^(.*?)\s*\[(\d+)]$""".toRegex()
/**
* Check if a messagebank entry exists based on messagedetail and languages
* @param messagedetail the messagedetail in format "message_name [ann_id]"
* @param languages a comma or semicolon separated string of languages to check
* @return true if the messagebank entry exists for all specified languages, false otherwise
*/
fun Messagebank_Exists(messagedetail: String, languages: String) : Boolean{
val match = messageDetailRegex.find(messagedetail)
val ll = languages.split(",",";").map { it.trim() }
if (match != null){
val msg = match.groupValues[1].trim()
val annid = match.groupValues[2].toUIntOrNull() ?: return false // kalau bukan number, return false
val ff = List.filter{ it.ANN_ID == annid && it.Description == msg }
return ll.all{ lang -> ff.any{ it.Language.equals(lang, ignoreCase = true) } }
}
return false
}
/**
* Find a messagebank entry based on messagedetail and language
* @param messagedetail the messagedetail in format "message_name [ann_id]"
* @param language the language to find
* @return the messagebank entry if found, null otherwise
*/
fun Find_Messagebank(messagedetail: String, language: String) : Messagebank?{
return try{
val match = messageDetailRegex.find(messagedetail)
if (match != null){
val msg = match.groupValues[1].trim()
val annid = match.groupValues[2].toUIntOrNull() ?: return null // kalau bukan number, return null
List.firstOrNull{ it.ANN_ID == annid && it.Description == msg && it.Language.equals(language, ignoreCase = true) }
} else {
null
}
} catch (_: Exception){
null
}
}
}

View File

@@ -0,0 +1,156 @@
package database.table
import database.data.QueuePaging
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
class Table_QueuePaging(connection : Connection) : dbFunctions<QueuePaging>("queue_paging", connection, listOf("index", "Date_Time", "Source", "Type", "Message", "BroadcastZones")) {
override fun Create() {
val tabledefinition ="CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"Date_Time VARCHAR(45) NOT NULL," + // Date and time of the entry
"Source VARCHAR(45) NOT NULL," + // Source of the entry
"Type VARCHAR(45) NOT NULL," + // Type of the entry
"Message VARCHAR(1024) NOT NULL," + // Message content
"BroadcastZones VARCHAR(1024)" + // Comma-separated soundbank tags
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
val queueList = ArrayList<QueuePaging>()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val queuePaging = QueuePaging(
resultSet.getLong("index").toUInt(),
resultSet.getString("Date_Time"),
resultSet.getString("Source"),
resultSet.getString("Type"),
resultSet.getString("Message"),
resultSet.getString("BroadcastZones"),
)
queueList.add(queuePaging)
List.add(queuePaging)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching ${super.dbName} : ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName} : ${e.message}" )
}
}
override fun Add(data: QueuePaging): Boolean {
try {
val statement = connection.prepareStatement(
"INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, BroadcastZones) VALUES (?, ?, ?, ?, ?)"
)
statement?.setString(1, data.Date_Time)
statement?.setString(2, data.Source)
statement?.setString(3, data.Type)
statement?.setString(4, data.Message)
statement?.setString(5, data.BroadcastZones)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("QueuePaging added: ${data.Message}" as Any)
return true
} else {
Logger.warn("No QueuePaging entry added for: ${data.Message}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding QueuePaging entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<QueuePaging>): Boolean {
return try {
connection.autoCommit = false
val sql =
"INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, BroadcastZones) VALUES (?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (qp in data) {
statement.setString(1, qp.Date_Time)
statement.setString(2, qp.Source)
statement.setString(3, qp.Type)
statement.setString(4, qp.Message)
statement.setString(5, qp.BroadcastZones)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk QueuePaging insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
true
} catch (e: Exception) {
Logger.error("Error adding QueuePaging entries: ${e.message}" as Any)
false
}
}
override fun UpdateByIndex(index: Int, data: QueuePaging): Boolean {
throw Exception("Update not supported")
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Date_Time, Source, Type, Message, BroadcastZones) SELECT Date_Time, Source, Type, Message, BroadcastZones FROM ${super.dbName} ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, BroadcastZones) SELECT Date_Time, Source, Type, Message, BroadcastZones FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by index: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
throw Exception("Importing QueuePaging from XLSX is not supported")
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("QueuePaging")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "Date_Time", "Source", "Type", "Message", "BroadcastZones")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("Date_Time"))
row.createCell(2).setCellValue(resultSet.getString("Source"))
row.createCell(3).setCellValue(resultSet.getString("Type"))
row.createCell(4).setCellValue(resultSet.getString("Message"))
row.createCell(5).setCellValue(resultSet.getString("BroadcastZones"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting QueuePaging, Msg: ${e.message}" }
}
return null
}
}

View File

@@ -0,0 +1,181 @@
package database.table
import database.data.QueueTable
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
class Table_QueueSoundbank(connection: Connection) : dbFunctions<QueueTable>("queue_table", connection, listOf("index", "Date_Time", "Source", "Type", "Message", "SB_TAGS", "BroadcastZones", "Repeat", "Language")) {
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"Date_Time VARCHAR(45) NOT NULL," + // Date and time of the entry
"Source VARCHAR(45) NOT NULL," + // Source of the entry
"Type VARCHAR(45) NOT NULL," + // Type of the entry
"Message VARCHAR(1024) NOT NULL," + // Message content
"SB_TAGS VARCHAR(1024)," + // Comma-separated soundbank tags
"BroadcastZones VARCHAR(1024) NOT NULL," + // Comma-separated broadcast zones
"`Repeat` INT NOT NULL," + // Number of repeats
"Language VARCHAR(100) NOT NULL" + // Language of the message
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
val queueList = ArrayList<QueueTable>()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val queueTable = QueueTable(
resultSet.getLong("index").toUInt(),
resultSet.getString("Date_Time"),
resultSet.getString("Source"),
resultSet.getString("Type"),
resultSet.getString("Message"),
resultSet.getString("SB_TAGS"),
resultSet.getString("BroadcastZones"),
resultSet.getInt("Repeat").toUInt(),
resultSet.getString("Language")
)
queueList.add(queueTable)
List.add(queueTable)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching ${super.dbName} : ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName} : ${e.message}" )
}
}
override fun Add(data: QueueTable): Boolean {
try {
val statement = connection.prepareStatement(
"INSERT INTO ${super.dbName} (`Date_Time`, `Source`, `Type`, `Message`, `SB_TAGS`, `BroadcastZones`, `Repeat`, `Language`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
)
statement?.setString(1, data.Date_Time)
statement?.setString(2, data.Source)
statement?.setString(3, data.Type)
statement?.setString(4, data.Message)
statement?.setString(5, data.SB_TAGS)
statement?.setString(6, data.BroadcastZones)
statement?.setInt(7, data.Repeat.toInt())
statement?.setString(8, data.Language)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("QueueTable added Source=${data.Source} Type=${data.Type} Message=${data.Message}, Languages=${data.Language} Variables=${data.SB_TAGS}, BroadcastZones=${data.BroadcastZones}" as Any)
return true
} else {
Logger.warn("No QueueTable entry added for: ${data.Message}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding QueueTable entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<QueueTable>): Boolean {
try {
connection.autoCommit = false
val sql =
"INSERT INTO ${super.dbName} (`Date_Time`, `Source`, `Type`, `Message`, `SB_TAGS`, `BroadcastZones`, `Repeat`, `Language`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (qt in data) {
statement.setString(1, qt.Date_Time)
statement.setString(2, qt.Source)
statement.setString(3, qt.Type)
statement.setString(4, qt.Message)
statement.setString(5, qt.SB_TAGS)
statement.setString(6, qt.BroadcastZones)
statement.setInt(7, qt.Repeat.toInt())
statement.setString(8, qt.Language)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk QueueTable insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
return true
} catch (e: Exception) {
Logger.error("Error adding QueueTable entries: ${e.message}" as Any)
}
return false
}
override fun UpdateByIndex(index: Int, data: QueueTable): Boolean {
throw Exception("Update not supported")
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language FROM ${super.dbName} ORDER BY `index` ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, `Repeat`, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by index: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
throw Exception("Import XLSX not supported for QueueTable")
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("QueueTable")
val headerRow = sheet.createRow(0)
val headers = arrayOf(
"Index",
"Date_Time",
"Source",
"Type",
"Message",
"SB_TAGS",
"BroadcastZones",
"Repeat",
"Language"
)
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("Date_Time"))
row.createCell(2).setCellValue(resultSet.getString("Source"))
row.createCell(3).setCellValue(resultSet.getString("Type"))
row.createCell(4).setCellValue(resultSet.getString("Message"))
row.createCell(5).setCellValue(resultSet.getString("SB_TAGS"))
row.createCell(6).setCellValue(resultSet.getString("BroadcastZones"))
row.createCell(7).setCellValue(resultSet.getString("Repeat"))
row.createCell(8).setCellValue(resultSet.getString("Language"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting QueueTable, Msg: ${e.message}" }
}
return null
}
}

View File

@@ -0,0 +1,339 @@
package database.table
import codes.Somecodes.Companion.ValidLanguage
import codes.Somecodes.Companion.ValidScheduleDay
import codes.Somecodes.Companion.ValidScheduleTime
import content.ScheduleDay
import database.MariaDB.Companion.ValidTime
import database.data.ScheduleBank
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
import broadcastDB
import codes.Somecodes
import messageDB
import java.time.LocalDate
class Table_Schedule(connection: Connection) : dbFunctions<ScheduleBank>("schedulebank", connection, listOf("index", "Description", "Day", "Time", "Soundpath", "Repeat", "Enable", "BroadcastZones", "Language")) {
/**
* A list to hold today's schedule entries.
*/
val todaySchedule = ArrayList<ScheduleBank>()
/**
* Update today's schedule
*/
fun UpdateTodaySchedule(){
todaySchedule.clear()
fun Find_Enabled_Schedules() : List<ScheduleBank>{
return List
.asSequence()
.filter{it.Enable} // yang enabled saja
.filter{ValidScheduleTime(it.Time)} // yang timenya dalam format HH:MM
.filter{ValidLanguage(it.Language)} // yang bahasanya valid
.filter{broadcastDB.AllBroadcastZonesValid(it.BroadcastZones, false)} // yang broadcastzonesnya valid
// Soundpath ini coding typo, aslinya Messagebank description
.filter{messageDB.Messagebank_Exists(it.Soundpath, it.Language)}
.toList()
}
val eligibleSchedule = Find_Enabled_Schedules()
val tempMap = mutableMapOf<String, ScheduleBank>()
// prioritas paling rendah adalah everyday
eligibleSchedule.filter { it.Day == ScheduleDay.Everyday.day }.forEach { tempMap[it.Time] = it }
// lebih tinggi adalah weekly, akan replace everyday jika time nya sama
val today_DOW = ScheduleDay.from_LocalDate_DOW(LocalDate.now().dayOfWeek)
eligibleSchedule.filter { it.Day == today_DOW.day }.forEach { tempMap[it.Time] = it }
// paling tinggi adalah specific date, akan replace yang lain jika time nya sama
val today = Somecodes.Today_to_DateString()
eligibleSchedule.filter { it.Day == today }.forEach { tempMap[it.Time] = it }
// masukin ke todaySchedule yang sudah di sort by Time
todaySchedule.addAll(tempMap.values.sortedBy { it.Time })
}
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"Description VARCHAR(128) NOT NULL," + // Description of the schedule
"Day VARCHAR(255) NOT NULL," + // Day in format DD/MM/YYYY
"Time VARCHAR(20) NOT NULL," + // Time in format HH:MM:SS
"Soundpath VARCHAR(512) NOT NULL," + // Path to the sound file
"`Repeat` TINYINT UNSIGNED NOT NULL," + // Repeat type (0=Once, 1=Daily, 2=Weekly, 3=Monthly, 4=Yearly)
"Enable BOOLEAN NOT NULL," + // Enable or disable the schedule
"BroadcastZones TEXT NOT NULL," + // Comma-separated list of broadcast zones
"Language VARCHAR(45) NOT NULL" + // Language code (e.g., EN, FR)
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val schedulebank = ScheduleBank(
resultSet.getLong("index").toUInt(),
resultSet.getString("Description"),
resultSet.getString("Day"),
resultSet.getString("Time"),
resultSet.getString("Soundpath"),
resultSet.getInt("Repeat").toUByte(),
resultSet.getBoolean("Enable"),
resultSet.getString("BroadcastZones"),
resultSet.getString("Language")
)
List.add(schedulebank)
}
UpdateTodaySchedule()
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching ${super.dbName}: ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName}: ${e.message}" )
}
}
override fun Add(data: ScheduleBank): Boolean {
if (!ValidScheduleDay(data.Day)) {
Logger.error("Error adding schedulebank entry: Invalid date format ${data.Day}" as Any)
return false
}
if (!ValidTime(data.Time)) {
Logger.error("Error adding schedulebank entry: Invalid time format ${data.Time}" as Any)
return false
}
try {
val statement =
connection.prepareStatement("INSERT INTO ${super.dbName} (Description, Day, Time, Soundpath, `Repeat`, Enable, BroadcastZones, Language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
statement?.setString(1, data.Description)
statement?.setString(2, data.Day)
statement?.setString(3, data.Time)
statement?.setString(4, data.Soundpath)
statement?.setInt(5, data.Repeat.toInt())
statement?.setBoolean(6, data.Enable)
statement?.setString(7, data.BroadcastZones)
statement?.setString(8, data.Language)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Schedulebank added: ${data.Description}" as Any)
return true
} else {
Logger.warn("No schedulebank entry added for: ${data.Description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding schedulebank entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<ScheduleBank>): Boolean {
try {
connection.autoCommit = false
val sql =
"INSERT INTO ${super.dbName} (Description, Day, Time, Soundpath, `Repeat`, Enable, BroadcastZones, Language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (sb in data) {
if (!ValidScheduleDay(sb.Day) || !ValidTime(sb.Time)) {
Logger.error("Invalid date or time format for schedulebank: ${sb.Description}" as Any)
continue
}
statement.setString(1, sb.Description)
statement.setString(2, sb.Day)
statement.setString(3, sb.Time)
statement.setString(4, sb.Soundpath)
statement.setInt(5, sb.Repeat.toInt())
statement.setBoolean(6, sb.Enable)
statement.setString(7, sb.BroadcastZones)
statement.setString(8, sb.Language)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk schedulebank insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
return true
} catch (e: Exception) {
Logger.error("Error adding schedulebank entries: ${e.message}" as Any)
}
return false
}
override fun UpdateByIndex(index: Int, data: ScheduleBank): Boolean {
if (!ValidScheduleDay(data.Day)) {
Logger.error("Error updating schedulebank entry: Invalid date format ${data.Day}" as Any)
return false
}
if (!ValidTime(data.Time)) {
Logger.error("Error updating schedulebank entry: Invalid time format ${data.Time}" as Any)
return false
}
try {
val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET Description = ?, Day = ?, Time = ?, Soundpath = ?, `Repeat` = ?, Enable = ?, BroadcastZones = ?, Language = ? WHERE `index` = ?")
statement?.setString(1, data.Description)
statement?.setString(2, data.Day)
statement?.setString(3, data.Time)
statement?.setString(4, data.Soundpath)
statement?.setInt(5, data.Repeat.toInt())
statement?.setBoolean(6, data.Enable)
statement?.setString(7, data.BroadcastZones)
statement?.setString(8, data.Language)
statement?.setLong(9, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Schedulebank updated at index $index: ${data.Description}" as Any)
return true
} else {
Logger.warn("No schedulebank entry updated at index $index for: ${data.Description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating schedulebank entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Description, Day, Time, Soundpath, `Repeat`, Enable, BroadcastZones, Language) SELECT Description, Day, Time, Soundpath, `Repeat`, Enable, BroadcastZones, Language FROM ${super.dbName} ORDER BY Day , Time ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, Day, Time, Soundpath, `Repeat`, Enable, BroadcastZones, Language) SELECT Description, Day, Time, Soundpath, `Repeat`, Enable, BroadcastZones, Language FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by Day and Time" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by Day and Time: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
val sheet =
workbook.getSheet("Schedulebank") ?: throw Exception("No sheet named 'Schedulebank' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf(
"Index",
"Description",
"Day",
"Time",
"Soundpath",
"Repeat",
"Enable",
"BroadcastZones",
"Language"
)
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing schedulebank
Clear()
// read each row and insert into database
val _schedulebankList = ArrayList<ScheduleBank>()
//Logger.info{"Sheet last row num: ${sheet.lastRowNum}"}
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
//println(row)
val description = row.getCell(1)?.stringCellValue ?: continue
//println(description.toString())
val day = row.getCell(2)?.stringCellValue ?: continue
//println(day.toString())
val time = row.getCell(3)?.stringCellValue ?: continue
//println(time.toString())
val soundpath = row.getCell(4)?.stringCellValue ?: continue
//println(soundpath.toString())
val repeat = row.getCell(5)?.stringCellValue?.toUByteOrNull() ?: continue
// println(repeat.toString())
//val enable = row.getCell(6)?.stringCellValue?.toBooleanStrictOrNull() ?: continue
val enable = row.getCell(6)?.stringCellValue?.toBoolean() ?: continue
//println(enable.toString())
val broadcastZones = row.getCell(7)?.stringCellValue ?: continue
//println(broadcastZones.toString())
val language = row.getCell(8)?.stringCellValue ?: continue
//println(language.toString())
val schedulebank =
ScheduleBank(
0u,
description,
day,
time,
soundpath,
repeat,
enable,
broadcastZones,
language
)
Logger.info{"SchedulebankList added 1"}
_schedulebankList.add(schedulebank)
}
return AddAll(_schedulebankList)
} catch (e: Exception) {
Logger.error { "Error importing Schedulebank, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("Schedulebank")
val headerRow = sheet.createRow(0)
val headers = arrayOf(
"Index",
"Description",
"Day",
"Time",
"Soundpath",
"Repeat",
"Enable",
"BroadcastZones",
"Language"
)
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("Description"))
row.createCell(2).setCellValue(resultSet.getString("Day"))
row.createCell(3).setCellValue(resultSet.getString("Time"))
row.createCell(4).setCellValue(resultSet.getString("Soundpath"))
row.createCell(5).setCellValue(resultSet.getString("Repeat"))
row.createCell(6).setCellValue(resultSet.getString("Enable"))
row.createCell(7).setCellValue(resultSet.getString("BroadcastZones"))
row.createCell(8).setCellValue(resultSet.getString("Language"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting Schedulebank, Msg: ${e.message}" }
}
return null
}
}

View File

@@ -0,0 +1,256 @@
package database.table
import database.data.SoundChannel
import database.dbFunctions
import max_channel
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
class Table_SoundChannel(connection: Connection) : dbFunctions<SoundChannel>("soundchannel", connection, listOf("index", "channel", "ip")) {
override fun Create() {
val tableDefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"channel VARCHAR(45) NOT NULL," + // Channel 01 to Channel 64
"ip VARCHAR(45) NOT NULL" + // IP address or empty string
")"
super.Create(tableDefinition)
// Check if table is empty, if so, populate with 64 channels
try {
val statement = connection.createStatement()
val countResult = statement?.executeQuery("SELECT COUNT(*) AS count FROM ${super.dbName}")
if (countResult?.next() == true) {
val count = countResult.getInt("count")
if (count < max_channel) {
Logger.info("SoundChannel table is empty, populating with default channels" as Any)
Clear()
}
}
} catch (e: Exception) {
Logger.error("Error creating SoundChannel table: ${e.message}" as Any)
}
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName} ORDER BY `index` ")
while (resultSet?.next() == true) {
val channel = SoundChannel(
resultSet.getLong("index").toUInt(),
resultSet.getString("channel"),
resultSet.getString("ip")
)
List.add(channel)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching sound channels: ${e.message}" as Any)
cbFail?.accept("Error fetching sound channels: ${e.message}" )
}
}
override fun Add(data: SoundChannel): Boolean {
try {
val statement = connection.prepareStatement("UPDATE ${super.dbName} SET ip = ? WHERE channel = ?")
statement?.setString(1, data.ip)
statement?.setString(2, data.channel)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("SoundChannel updated: ${data.channel} -> ${data.ip}" as Any)
return true
} else {
Logger.warn("No SoundChannel entry updated for: ${data.channel} -> ${data.ip}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating SoundChannel entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<SoundChannel>): Boolean {
return try {
connection.autoCommit = false
val sql = "UPDATE ${super.dbName} SET ip = ? WHERE channel = ?"
val statement = connection.prepareStatement(sql)
for (sc in data) {
statement.setString(1, sc.ip)
statement.setString(2, sc.channel)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk SoundChannel update successful: ${data.size} entries" as Any)
connection.autoCommit = true
true
} catch (e: Exception) {
Logger.error("Error updating SoundChannel entries: ${e.message}" as Any)
false
}
}
override fun UpdateByIndex(index: Int, data: SoundChannel): Boolean {
try {
val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET channel = ?, ip = ? WHERE `index` = ?")
statement?.setString(1, data.channel)
statement?.setString(2, data.ip)
statement?.setLong(3, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("SoundChannel updated at index $index: ${data.channel} -> ${data.ip}" as Any)
return true
} else {
Logger.warn("No Sound Channel entry updated at index $index for: ${data.channel} -> ${data.ip}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating SoundChannel entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (channel, ip) SELECT channel, ip FROM ${super.dbName} ORDER BY `index` ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (channel, ip) SELECT channel, ip FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by index: ${e.message}" as Any)
}
return false
}
override fun Clear(): Boolean {
try {
val statement = connection.createStatement()
// use TRUNCATE to reset auto increment index
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
Logger.info("${super.dbName} table cleared" as Any)
List.clear()
// create new rows from 1 to 64 with description "Channel 1" to "Channel 64" and empty ip
for (i in 1..max_channel) {
val channel = String.format("Channel %d", i)
val insertStatement =
connection.prepareStatement("INSERT INTO ${super.dbName} (channel, ip) VALUES (?, ?)")
insertStatement?.setString(1, channel)
insertStatement?.setString(2, "")
insertStatement?.executeUpdate()
}
return true
} catch (e: Exception) {
Logger.error("Error clearing soundchannel table: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
val sheet =
workbook.getSheet("SoundChannel") ?: throw Exception("No sheet named 'SoundChannel' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf("Index", "channel", "ip")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing soundchannel
Clear()
// read each row and insert into database
val _soundChannelList = ArrayList<SoundChannel>()
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val channel = row.getCell(1)?.stringCellValue ?: continue
val ip = row.getCell(2)?.stringCellValue ?: continue
val soundChannel = SoundChannel(0u, channel, ip)
_soundChannelList.add(soundChannel)
}
// Bulk update IPs for channels
var success = true
for (sc in _soundChannelList) {
if (!Add(sc)) {
success = false
}
}
return success
} catch (e: Exception) {
Logger.error { "Error importing SoundChannel, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("SoundChannel")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "channel", "ip")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("channel"))
row.createCell(2).setCellValue(resultSet.getString("ip"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting SoundChannel, Msg: ${e.message}" }
}
return null
}
/**
* Delete entry by index, but only clear the IP field
* @param index The index of the entry to delete
* @return true if successful, false otherwise
*/
override fun DeleteByIndex(index: Int): Boolean {
try {
val statement = connection.prepareStatement("UPDATE ${super.dbName} SET ip = '' WHERE `index` = ?")
statement?.setLong(1, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("${super.dbName} IP cleared for index $index" as Any)
return true
} else {
Logger.warn("No ${super.dbName} entry cleared for index $index" as Any)
}
} catch (e: Exception) {
Logger.error("Error clearing ${super.dbName} entry for index $index: ${e.message}" as Any)
}
return false
}
/**
* Get all distinct sound channel from soundchannelDB
* @return a list of distinct sound channel sorted alphabetically
*/
fun Get_SoundChannel_List(): List<String> {
return List
.distinctBy { it.channel }
.map { it.channel }
.sorted()
}
}

View File

@@ -0,0 +1,349 @@
package database.table
import content.Category
import database.data.Soundbank
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.io.File
import java.sql.Connection
import java.util.function.Consumer
class Table_Soundbank(connection: Connection) : dbFunctions<Soundbank>("soundbank", connection, listOf("index", "Description", "TAG", "Category", "Language", "VoiceType", "Path")) {
override fun Create() {
val tabledefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"Description VARCHAR(1024) NOT NULL," + // Description of the soundbank
"TAG VARCHAR(45) NOT NULL," + // TAG of the soundbank
"Category VARCHAR(45) NOT NULL," + // Category of the soundbank
"Language VARCHAR(45) NOT NULL," + // Language of the soundbank
"VoiceType VARCHAR(45) NOT NULL," + // VoiceType of the soundbank
"Path VARCHAR(1024) NOT NULL" + // Path to the sound file
")"
super.Create(tabledefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName} ORDER BY Category, Language, VoiceType, TAG")
while (resultSet?.next() == true) {
val soundbank = Soundbank(
resultSet.getLong("index").toUInt(),
resultSet.getString("Description"),
resultSet.getString("TAG"),
resultSet.getString("Category"),
resultSet.getString("Language"),
resultSet.getString("VoiceType"),
resultSet.getString("Path")
)
List.add(soundbank)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching soundbanks: ${e.message}" as Any)
cbFail?.accept("Error fetching ${super.dbName} : ${e.message}")
}
}
override fun Add(data: Soundbank): Boolean {
try {
val statement =
connection.prepareStatement("INSERT INTO ${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) VALUES (?, ?, ?, ?, ?, ?)")
statement?.setString(1, data.Description)
statement?.setString(2, data.TAG)
statement?.setString(3, data.Category)
statement?.setString(4, data.Language)
statement?.setString(5, data.VoiceType)
statement?.setString(6, data.Path)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Soundbank added: ${data.Description}" as Any)
return true
} else {
Logger.warn("No soundbank entry added for: ${data.Description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding soundbank entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<Soundbank>): Boolean {
// use mysql bulk insert
try {
connection.autoCommit = false
val sql =
"INSERT INTO ${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) VALUES (?, ?, ?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (sb in data) {
statement.setString(1, sb.Description)
statement.setString(2, sb.TAG)
statement.setString(3, sb.Category)
statement.setString(4, sb.Language)
statement.setString(5, sb.VoiceType)
statement.setString(6, sb.Path)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk soundbank insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
return true
} catch (e: Exception) {
Logger.error("Error adding soundbank entries: ${e.message}" as Any)
}
return false
}
override fun UpdateByIndex(index: Int, data: Soundbank): Boolean {
try {
val statement =
connection.prepareStatement("UPDATE ${super.dbName} SET Description = ?, TAG = ?, Category = ?, Language = ?, VoiceType = ?, Path = ? WHERE `index` = ?")
statement?.setString(1, data.Description)
statement?.setString(2, data.TAG)
statement?.setString(3, data.Category)
statement?.setString(4, data.Language)
statement?.setString(5, data.VoiceType)
statement?.setString(6, data.Path)
statement?.setLong(7, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Soundbank updated at index $index: ${data.Description}" as Any)
return true
} else {
Logger.warn("No soundbank entry updated at index $index for: ${data.Description}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating soundbank entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM ${super.dbName} ORDER BY Category, Language, VoiceType, TAG ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (Description, TAG, Category, Language, VoiceType, Path) SELECT Description, TAG, Category, Language, VoiceType, Path FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by Description" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by Description: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
// check if there is sheet named "Soundbank"
val sheet =
workbook.getSheet("Soundbank") ?: throw Exception("No sheet named 'Soundbank' found")
// check if the sheet contains header named "index", "Description", "TAG", "Category", "Language", "VoiceType", "Path"
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers =
arrayOf("Index", "Description", "TAG", "Category", "Language", "VoiceType", "Path")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing soundbank
Clear()
// read each row and insert into database
val _soundbankList = ArrayList<Soundbank>()
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val description = row.getCell(1)?.stringCellValue ?: continue
val tag = row.getCell(2)?.stringCellValue ?: continue
val category = row.getCell(3)?.stringCellValue ?: continue
val language = row.getCell(4)?.stringCellValue ?: continue
val voiceType = row.getCell(5)?.stringCellValue ?: continue
val path = row.getCell(6)?.stringCellValue ?: continue
val soundbank = Soundbank(0u, description, tag, category, language, voiceType, path)
_soundbankList.add(soundbank)
}
return AddAll(_soundbankList)
} catch (e: Exception) {
Logger.error { "Error importing Soundbank, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("Soundbank")
val headerRow = sheet.createRow(0)
val headers =
arrayOf("Index", "Description", "TAG", "Category", "Language", "VoiceType", "Path")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("Description"))
row.createCell(2).setCellValue(resultSet.getString("TAG"))
row.createCell(3).setCellValue(resultSet.getString("Category"))
row.createCell(4).setCellValue(resultSet.getString("Language"))
row.createCell(5).setCellValue(resultSet.getString("VoiceType"))
row.createCell(6).setCellValue(resultSet.getString("Path"))
}
for (i in 0 until headers.size) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting Soundbank, Msg: ${e.message}" }
}
return null
}
/**
* Get all distinct airline code tags from soundbank
* @return a list of distinct airline code tags sorted alphabetically
*/
fun Get_AirlineCode_Tags(): List<String> {
return List
.filter { it.Category.equals(Category.Airline_Code.name,true) }
.distinctBy { it.TAG }
.map { it.TAG }
.sorted()
}
/**
* Get all distinct city tags from soundbank
* @return a list of distinct city tags sorted alphabetically
*/
fun Get_City_Tags(): List<String> {
return List
.filter { it.Category.equals(Category.City.name,true) }
.distinctBy { it.TAG }
.map { it.TAG }
.sorted()
}
/**
* Find City by TAG
* @param tag the city tag to search for
* @return a list of Soundbank entries matching the city tag
*/
fun Find_City_By_TAG(tag: String) : List<Soundbank> {
return List
.filter {it.Category.equals(Category.City.name,true) }
.filter { it.TAG.equals(tag, true) }
.distinctBy { it.TAG }
}
/**
* Find Airline Name by TAG
* @param tag the airline code tag to search for
* @return a list of Soundbank entries matching the airline code tag
*/
fun Find_AirlineName_By_TAG(tag: String) : List<Soundbank> {
return List
.filter {it.Category.equals(Category.Airplane_Name.name,true) }
.filter { it.TAG.equals(tag, true) }
.distinctBy { it.TAG }
}
fun Get_Places(): List<Soundbank> {
return List
.filter { it.Category.equals(Category.Places.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Shalat() : List<Soundbank> {
return List
.filter { it.Category.equals(Category.Shalat.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Sequences() : List<Soundbank> {
return List
.filter { it.Category.equals(Category.Sequence.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Reasons() : List<Soundbank> {
return List
.filter { it.Category.equals(Category.Reason.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Gates(): List<Soundbank> {
return List
.filter { it.Category.equals(Category.Gate.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Compensation(): List<Soundbank> {
return List
.filter { it.Category.equals(Category.Compensation.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Greeting() : List<Soundbank> {
return List
.filter { it.Category.equals(Category.Greeting.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
fun Get_Procedures(): List<Soundbank> {
return List
.filter { it.Category.equals(Category.Procedure.name,true) }
.distinctBy { it.TAG }
.sortedBy { it.TAG }
}
class FileCheckResult{
var validfile = arrayListOf<Soundbank>()
var invalidfile = arrayListOf<Soundbank>()
}
/**
* Check Soundbank path files existence
* @param cb callback with FileCheckResult containing valid and invalid files
*/
fun FileCheck(cb: Consumer<FileCheckResult>){
val result = FileCheckResult()
// file wav must at least 10 kb
val validfilesize = 10 * 1024; // 10 KB
for (sb in List){
if (sb.Path.isBlank()) {
result.invalidfile.add(sb)
continue
}
val file = File(sb.Path)
if (file.isFile && file.length() >= validfilesize) {
// file size should at leat 10 kb
result.validfile.add(sb)
} else {
result.invalidfile.add(sb)
}
}
cb.accept(result)
}
}

View File

@@ -0,0 +1,225 @@
package database.table
import database.data.UserDB
import database.dbFunctions
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.util.function.Consumer
class Table_Users(connection : Connection) : dbFunctions<UserDB>("newuser", connection, listOf("index", "username", "password", "location", "airline_tags", "city_tags", "messagebank_ann_id", "broadcastzones")) {
override fun Create() {
val tableDefinition = "CREATE TABLE IF NOT EXISTS ${super.dbName} (" +
"`index` INT AUTO_INCREMENT PRIMARY KEY," +
"username VARCHAR(100) NOT NULL," +
"password VARCHAR(100) NOT NULL," +
"location VARCHAR(100) NOT NULL," +
"airline_tags TEXT NOT NULL,"+ // Comma-separated soundbank tags
"city_tags TEXT NOT NULL,"+ // Comma-separated soundbank tags
"messagebank_ann_id TEXT NOT NULL,"+ // Comma-separated messagebank announcement index
"broadcastzones TEXT NOT NULL"+ // Comma-separated broadcast zones
")"
super.Create(tableDefinition)
}
override fun Get(cbOK: Consumer<Unit>?, cbFail: Consumer<String>?) {
List.clear()
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
while (resultSet?.next() == true) {
val user = UserDB(
resultSet.getLong("index").toUInt(),
resultSet.getString("username"),
resultSet.getString("password"),
resultSet.getString("location"),
resultSet.getString("airline_tags"),
resultSet.getString("city_tags"),
resultSet.getString("messagebank_ann_id"),
resultSet.getString("broadcastzones")
)
List.add(user)
}
cbOK?.accept(Unit)
} catch (e: Exception) {
Logger.error("Error fetching users: ${e.message}" as Any)
cbFail?.accept("Error fetching users: ${e.message}" )
}
}
override fun Add(data: UserDB): Boolean {
try {
val statement = connection.prepareStatement("INSERT INTO ${super.dbName} (username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones) VALUES (?, ?, ?, ?,?, ?, ?)")
statement?.setString(1, data.username)
statement?.setString(2, data.password)
statement?.setString(3, data.location)
statement?.setString(4, data.airline_tags)
statement?.setString(5, data.city_tags)
statement?.setString(6, data.messagebank_ann_id)
statement?.setString(7, data.broadcastzones)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("User added: ${data.username}" as Any)
return true
} else {
Logger.warn("No user entry added for: ${data.username}" as Any)
}
} catch (e: Exception) {
Logger.error("Error adding user entry: ${e.message}" as Any)
}
return false
}
override fun AddAll(data: ArrayList<UserDB>): Boolean {
return try {
connection.autoCommit = false
val sql = "INSERT INTO ${super.dbName} (username, password, location,airline_tags,city_tags, messagebank_ann_id, broadcastzones) VALUES (?, ?, ?,?, ?, ?, ?)"
val statement = connection.prepareStatement(sql)
for (user in data) {
statement.setString(1, user.username)
statement.setString(2, user.password)
statement.setString(3, user.location)
statement.setString(4, user.airline_tags)
statement.setString(5, user.city_tags)
statement.setString(6, user.messagebank_ann_id)
statement.setString(7, user.broadcastzones)
statement.addBatch()
}
statement.executeBatch()
connection.commit()
Logger.info("Bulk user insert successful: ${data.size} entries" as Any)
connection.autoCommit = true
true
} catch (e: Exception) {
Logger.error("Error adding user entries: ${e.message}" as Any)
false
}
}
override fun UpdateByIndex(index: Int, data: UserDB): Boolean {
try {
val statement = connection.prepareStatement("UPDATE ${super.dbName} SET username = ?, password = ?, location = ?, airline_tags = ?,city_tags=?, messagebank_ann_id = ?, broadcastzones = ? WHERE `index` = ?")
statement?.setString(1, data.username)
statement?.setString(2, data.password)
statement?.setString(3, data.location)
statement?.setString(4, data.airline_tags)
statement?.setString(5, data.city_tags)
statement?.setString(6, data.messagebank_ann_id)
statement?.setString(7, data.broadcastzones)
statement?.setLong(8, index.toLong())
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("User updated at index $index: ${data.username}" as Any)
return true
} else {
Logger.warn("No user entry updated at index $index for: ${data.username}" as Any)
}
} catch (e: Exception) {
Logger.error("Error updating user entry at index $index: ${e.message}" as Any)
}
return false
}
override fun Resort(): Boolean {
try {
val statement = connection.createStatement()
val tempdb_name = "temp_${super.dbName}"
// use a temporary table to reorder the index
statement?.executeUpdate("CREATE TABLE IF NOT EXISTS $tempdb_name LIKE ${super.dbName}")
statement?.executeUpdate("TRUNCATE TABLE $tempdb_name")
statement?.executeUpdate("INSERT INTO $tempdb_name (username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones) SELECT username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones FROM ${super.dbName} ORDER BY username ")
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
statement?.executeUpdate("INSERT INTO ${super.dbName} (username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones) SELECT username, password, location, airline_tags, city_tags, messagebank_ann_id, broadcastzones FROM $tempdb_name")
statement?.executeUpdate("DROP TABLE $tempdb_name")
Logger.info("${super.dbName} table resorted by index" as Any)
// reload the local list
Get()
return true
} catch (e: Exception) {
Logger.error("Error resorting ${super.dbName} table by index: ${e.message}" as Any)
}
return false
}
override fun Import_XLSX(workbook: XSSFWorkbook): Boolean {
try {
val sheet = workbook.getSheet("User") ?: throw Exception("No sheet named 'User' found")
val headerRow = sheet.getRow(0) ?: throw Exception("No header row found")
val headers = arrayOf("Index", "username", "password", "location", "airline_tags", "city_tags", "messagebank_ann_id", "broadcastzones")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.getCell(colIndex) ?: throw Exception("Header '$header' not found")
if (cell.stringCellValue != header) throw Exception("Header '$header' not found")
}
// clear existing users
Clear()
// read each row and insert into database
val _userList = ArrayList<UserDB>()
for (rowIndex in 1..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val username = row.getCell(1)?.stringCellValue ?: continue
val password = row.getCell(2)?.stringCellValue ?: continue
val location = row.getCell(3)?.stringCellValue ?: continue
val airline_tags = row.getCell(4)?.stringCellValue ?: continue
val city_tags = row.getCell(5)?.stringCellValue ?: continue
val messagebank_ann_id = row.getCell(6)?.stringCellValue ?: continue
val broadcastzones = row.getCell(7)?.stringCellValue ?: continue
val user = UserDB(
0u,
username,
password,
location,
airline_tags,
city_tags,
messagebank_ann_id,
broadcastzones
)
_userList.add(user)
}
return AddAll(_userList)
} catch (e: Exception) {
Logger.error { "Error importing User, Msg: ${e.message}" }
}
return false
}
override fun Export_XLSX(): XSSFWorkbook? {
try {
val statement = connection.createStatement()
val resultSet = statement?.executeQuery("SELECT * FROM ${super.dbName}")
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("User")
val headerRow = sheet.createRow(0)
val headers = arrayOf("Index", "username", "password", "location", "airline_tags","city_tags", "messagebank_ann_id", "broadcastzones")
for ((colIndex, header) in headers.withIndex()) {
val cell = headerRow.createCell(colIndex)
cell.setCellValue(header)
}
var rowIndex = 1
while (resultSet?.next() == true) {
val row = sheet.createRow(rowIndex++)
row.createCell(0).setCellValue(resultSet.getString("index"))
row.createCell(1).setCellValue(resultSet.getString("username"))
row.createCell(2).setCellValue(resultSet.getString("password"))
row.createCell(3).setCellValue(resultSet.getString("location"))
row.createCell(4).setCellValue(resultSet.getString("airline_tags"))
row.createCell(5).setCellValue(resultSet.getString("city_tags"))
row.createCell(6).setCellValue(resultSet.getString("messagebank_ann_id"))
row.createCell(7).setCellValue(resultSet.getString("broadcastzones"))
}
for (i in headers.indices) {
sheet.autoSizeColumn(i)
}
return workbook
} catch (e: Exception) {
Logger.error { "Error exporting User, Msg: ${e.message}" }
}
return null
}
/**
* Check if a username already exists in the userDB (case-insensitive)
*/
fun Username_exists(username: String): Boolean {
return List.any { it.username.equals(username, ignoreCase = true) }
}
}

View File

@@ -9,7 +9,7 @@ import com.google.cloud.texttospeech.v1.VoiceSelectionParams
import content.Category
import content.Language
import content.VoiceType
import database.Soundbank
import database.data.Soundbank
import org.tinylog.Logger
import java.nio.file.Files
import java.util.function.BiConsumer

View File

@@ -0,0 +1,37 @@
package ourAirport
import codes.Somecodes
data class AirportData(val description: String, val latitude: Double, val longitude: Double, val country: String, val IATA: String, val ICAO: String)
{
companion object{
/**
* create AirportData from CSV line
* CSV format : id,ident,type,name,latitude_deg,longitude_deg,elevation_ft,continent,country_name,iso_country,region_name,iso_region,local_region,municipality,scheduled_service,gps_code,icao_code,iata_code,local_code,home_link,wikipedia_link,keywords,score,last_updated
* @param line CSV line
* @return AirportData or null if failed
*/
fun fromString(line: String) : AirportData? {
if (Somecodes.Companion.ValidString(line)){
try{
val values = line.split(",")
// description on index 3
val description = values[3].trim()
// latitude on index 4
val latitude = values[4].toDoubleOrNull() ?: return null
// longitude on index 5
val longitude = values[5].toDoubleOrNull() ?: return null
// country on index 8
val country = values[8].trim()
// ICAO on index 16
val ICAO = values[16].trim()
// IATA on index 17
val IATA = values[17].trim()
return AirportData(description, latitude, longitude, country, IATA, ICAO)
} catch (_ : Exception){
}
}
return null
}
}
}

View File

@@ -0,0 +1,47 @@
package ourAirport
import codes.Somecodes
import org.tinylog.Logger
import java.nio.file.Files
import kotlin.io.path.Path
/**
* this class read CSV from world-airports.csv included in resources folder
* and filter only the airports in the InterestedCountries list
* @param InterestedCountries vararg list of country names to filter airports. Default is "Indonesia"
*/
@Suppress("unused")
class OurAirport(vararg InterestedCountries: String = arrayOf("Indonesia")) {
val List: MutableList<AirportData> = mutableListOf()
init{
// extract world-airports.csv from resources to current folder
try{
val current = Path(Somecodes.current_directory)
if (!Files.exists(current.resolve("world-airports.csv"))){
val inputStream = this::class.java.getResourceAsStream("/world-airports.csv")
if (inputStream != null) {
Files.copy(inputStream, current)
Logger.info { "Extracted world-airports.csv to ${Somecodes.current_directory}" }
} else throw Exception("Resource world-airports.csv not found")
}
val lines = Files.readAllLines(current.resolve("world-airports.csv"))
for (line in lines.drop(1)) { // skip header
AirportData.fromString(line)?.let { ad ->
if (InterestedCountries.contains(ad.country)) {
List.add(ad)
}
}
}
}catch(ex: Exception){
Logger.error { "Failed to copy world-airports.csv: ${ex.message}" }
}
}
fun GetFromIATA(iata: String): AirportData? {
return List.find { it.IATA.equals(iata, ignoreCase = true) }
}
fun GetFromICAO(icao: String): AirportData? {
return List.find { it.ICAO.equals(icao, ignoreCase = true) }
}
}

View File

@@ -1,9 +1,9 @@
package web
class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, var isPlaying: Boolean) {
data class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, val isPlaying: Boolean, val filename: String, val duration: String, val elapsed: String, val broadcastzones: String) {
companion object{
fun fromBarixConnection(bc: barix.BarixConnection): StreamerOutputData {
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying())
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying(), bc.GetAudioFileInfo()?.fileName ?: "", bc.GetAudioFileInfo()?.DurationToString() ?: "", bc.GetElapsed(), bc.GetUsedByBroadcastZones())
}
}
}

File diff suppressed because it is too large Load Diff