first plugins for Ulanzi D200H

- petrol watch
- copilot usage

Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
2026-04-09 20:42:31 +02:00
commit fbf40f75b2
32 changed files with 2678 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.claude/settings.local.json
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 196">
<rect width="196" height="196" rx="20" fill="#1a1a2e"/>
<g transform="translate(44, 44) scale(0.212)">
<g>
<path style="fill:#FFFFFF;" d="M309.189,253.5c7.8,0,14.1-6.3,14.1-14.1V42.3c0-7.8-6.3-14.1-14.1-14.1h-227.1 c-7.8,0-14.1,6.3-14.1,14.1v197.1c0,7.8,6.3,14.1,14.1,14.1L309.189,253.5L309.189,253.5z"/>
<polygon style="fill:#FFFFFF;" points="109.189,281.8 128.289,479.8 262.989,479.8 282.089,281.8 "/>
</g>
<path style="fill:#FFC10D;" d="M140.289,310l1,10.6h64.6c7.8,0,14.1,6.3,14.1,14.1c0,7.8-6.3,14.1-14.1,14.1h-61.9l3.1,32h44.2 c7.8,0,14.1,6.3,14.1,14.1c0,7.8-6.3,14.1-14.1,14.1h-41.4l4.1,42.6h83.5l13.5-141.6H140.289z"/>
<path style="fill:#194F82;" d="M465.089,125.4l-42.1-48.9c-5.1-5.9-14-6.6-19.9-1.5s-6.6,14-1.5,19.9l38.7,45v252.6 c0,20.2-16.4,36.5-36.5,36.5s-36.5-16.4-36.5-36.5v-20.6c0-35.3-25.2-64.7-58.5-71.4l1.8-18.8c22.8-0.6,41.1-19.3,41.1-42.3V42.3 c0-23.3-19-42.3-42.3-42.3h-227.3c-23.4,0-42.4,19-42.4,42.3v197.1c0,22.9,18.3,41.6,41.1,42.3l20.5,213.5c0.7,7.2,6.8,12.8,14,12.8 h160.4c7.3,0,13.3-5.5,14-12.8l16-166.4c19,5.1,33,22.4,33,43v20.6c0,35.7,29.1,64.8,64.8,64.8s64.8-29.1,64.8-64.8V134.6 C468.489,131.3,467.289,128,465.089,125.4z M262.989,479.8h-134.7l-19.1-198h172.9L262.989,479.8z M82.089,253.5 c-7.8,0-14.1-6.3-14.1-14.1V42.3c0-7.8,6.3-14.1,14.1-14.1h227.2c7.8,0,14.1,6.3,14.1,14.1v197.1c0,7.8-6.3,14.1-14.1,14.1 L82.089,253.5L82.089,253.5z"/>
<path style="fill:#56ACE0;" d="M158.089,179.4h-33.7v17.7h142.5V84.7h-142.5v17.7h49.9c7.8,0,14.1,6.3,14.1,14.1 s-6.3,14.1-14.1,14.1h-49.9v20.7h33.7c7.8,0,14.1,6.3,14.1,14.1S165.889,179.4,158.089,179.4z"/>
<path style="fill:#194F82;" d="M280.989,56.4h-170.7c-7.8,0-14.1,6.3-14.1,14.1v140.7c0,7.8,6.3,14.1,14.1,14.1h170.7 c7.8,0,14.1-6.3,14.1-14.1V70.6C295.089,62.8,288.789,56.4,280.989,56.4z M266.889,197.1h-142.5v-17.7h33.7 c7.8,0,14.1-6.3,14.1-14.1s-6.3-14.1-14.1-14.1h-33.7v-20.7h49.9c7.8,0,14.1-6.3,14.1-14.1s-6.3-14.1-14.1-14.1h-49.9V84.7h142.5 L266.889,197.1L266.889,197.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+15
View File
@@ -0,0 +1,15 @@
{
"Localization": {
"URL": "URL",
"Station UUID": "Stations-UUID",
"Fuel Type": "Kraftstofftyp",
"Refresh Rate": "Aktualisierungsrate",
"On Press": "Bei Tastendruck",
"Every 1 min": "Jede Minute",
"Every 2 min": "Alle 2 Minuten",
"Every 5 min": "Alle 5 Minuten",
"Every 10 min": "Alle 10 Minuten",
"Every 30 min": "Alle 30 Minuten",
"Every Hour": "Jede Stunde"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"Localization": {
"URL": "URL",
"Station UUID": "Station UUID",
"Fuel Type": "Fuel Type",
"Refresh Rate": "Refresh Rate",
"On Press": "On Press",
"Every 1 min": "Every 1 min",
"Every 2 min": "Every 2 min",
"Every 5 min": "Every 5 min",
"Every 10 min": "Every 10 min",
"Every 30 min": "Every 30 min",
"Every Hour": "Every Hour"
}
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2C16.2091 2 18 3.79086 18 6V14C18 16.2091 16.2091 18 14 18H6C3.79086 18 2 16.2091 2 14V6C2 3.79086 3.79086 2 6 2H14ZM14.5332 7.2168C14.1659 6.84962 13.5704 6.84954 13.2031 7.2168L8.85254 11.5674L7.00977 9.72461C6.64259 9.35743 6.047 9.35766 5.67969 9.72461C5.3124 10.0919 5.3124 10.6874 5.67969 11.0547L8.1875 13.5625C8.55479 13.9298 9.15029 13.9298 9.51758 13.5625L14.5332 8.54688C14.9005 8.17959 14.9005 7.58408 14.5332 7.2168Z" fill="#00FFE6"/>
</svg>

After

Width:  |  Height:  |  Size: 567 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.8" y="2.8" width="14.4" height="14.4" rx="3.2" stroke="white" stroke-width="1.6" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2C12.2091 2 14.2095 2.89506 15.6572 4.34277C17.1049 5.79051 18 7.79088 18 10C18 12.2091 17.1049 14.2095 15.6572 15.6572C14.2095 17.1049 12.2091 18 10 18C7.79088 18 5.79051 17.1049 4.34277 15.6572C2.89506 14.2095 2 12.2091 2 10C2 7.79088 2.89506 5.79051 4.34277 4.34277C5.79051 2.89506 7.79088 2 10 2ZM14.5332 7.2168C14.1659 6.84967 13.5704 6.84956 13.2031 7.2168L8.85254 11.5674L7.00977 9.72461C6.64257 9.35742 6.047 9.35761 5.67969 9.72461C5.3124 10.0919 5.3124 10.6874 5.67969 11.0547L8.1875 13.5625C8.55479 13.9298 9.15029 13.9298 9.51758 13.5625L14.5332 8.54688C14.9005 8.17959 14.9005 7.58408 14.5332 7.2168Z" fill="#00FFE6"/>
</svg>

After

Width:  |  Height:  |  Size: 751 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99902 2.79993C11.9874 2.79993 13.786 3.60548 15.0898 4.9093C16.3937 6.21315 17.1992 8.0117 17.1992 10.0001C17.1992 11.9885 16.3937 13.7871 15.0898 15.0909C13.786 16.3948 11.9874 17.2003 9.99902 17.2003C8.01061 17.2003 6.21205 16.3948 4.9082 15.0909C3.60438 13.7871 2.79883 11.9885 2.79883 10.0001C2.79883 8.0117 3.60438 6.21315 4.9082 4.9093C6.21205 3.60548 8.0106 2.79993 9.99902 2.79993Z" stroke="white" stroke-width="1.6" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99902 2.79993C11.9874 2.79993 13.786 3.60548 15.0898 4.9093C16.3937 6.21315 17.1992 8.0117 17.1992 10.0001C17.1992 11.9885 16.3937 13.7871 15.0898 15.0909C13.786 16.3948 11.9874 17.2003 9.99902 17.2003C8.01061 17.2003 6.21205 16.3948 4.9082 15.0909C3.60438 13.7871 2.79883 11.9885 2.79883 10.0001C2.79883 8.0117 3.60438 6.21315 4.9082 4.9093C6.21205 3.60548 8.0106 2.79993 9.99902 2.79993Z" stroke="#00FFE6" stroke-width="1.6" stroke-linejoin="round"/>
<path d="M10 13C11.6569 13 13 11.6569 13 10C13 8.34315 11.6569 7 10 7C8.34315 7 7 8.34315 7 10C7 11.6569 8.34315 13 10 13Z" fill="#00FFE6"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="#A6A6A6" xmlns="http://www.w3.org/2000/svg">
<path d="M17.8541 7.96348C18.2641 7.55346 18.9287 7.55352 19.3387 7.96348C19.7488 8.37353 19.7488 9.03808 19.3387 9.44813L12.6329 16.1539C12.2229 16.564 11.5584 16.564 11.1483 16.1539L4.44253 9.44813C4.03248 9.03808 4.03248 8.37353 4.44253 7.96348C4.85258 7.55352 5.51715 7.55346 5.92717 7.96348L11.8906 13.9269L17.8541 7.96348Z" />
</svg>

After

Width:  |  Height:  |  Size: 442 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5986 3.00488C14.8276 3.02757 15.0429 3.12883 15.207 3.29297L19.707 7.29297L19.7734 7.36621C19.9193 7.54417 20 7.76791 20 8V19.25C20 20.2165 19.2165 21 18.25 21H5.75C4.7238 21 4.0001 20.1325 4 19.2002V4.7998C4.0001 3.86756 4.72379 3.00003 5.75 3H14.5L14.5986 3.00488ZM6 19H18V10.5H13.5C12.9477 10.5 12.5 10.0523 12.5 9.5V5H6V19ZM14.5 8.5H18V8.41406L14.5 5.36035V8.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19H19V8.5H12C11.7429 8.5 11.4972 8.40143 11.3125 8.22656L11.2373 8.14746L9.43164 5H5V19ZM21 19.1729C20.9998 20.1982 20.1588 20.9998 19.1582 21H4.8418C3.84119 20.9998 3.00024 20.1982 3 19.1729V4.82715C3.00024 3.80179 3.84118 3.00016 4.8418 3H9.89453L10.0039 3.00586C10.2568 3.03361 10.4909 3.15741 10.6572 3.35352L12.4629 6.5H19.1582C20.159 6.50016 21 7.30255 21 8.32812V19.1729Z" fill="white"/>
<path d="M15 15C15.5424 15.0258 16 15.4351 16 16C16 16.5649 15.5424 16.9742 15 17H9C8.44772 17 8 16.5523 8 16C8 15.4477 8.44772 15 9 15H15Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 670 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="21" height="19" viewBox="0 0 21 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.692 15.3721L12.4449 1.12207C12.2471 0.780801 11.963 0.497514 11.6212 0.300585C11.2794 0.103656 10.8919 0 10.4974 0C10.1029 0 9.7154 0.103656 9.3736 0.300585C9.0318 0.497514 8.74775 0.780801 8.54991 1.12207V1.1228L0.302837 15.3721C0.104832 15.714 0.00038348 16.1021 1.05394e-06 16.4973C-0.000381372 16.8924 0.103316 17.2807 0.300659 17.623C0.498001 17.9654 0.782029 18.2497 1.12416 18.4474C1.46629 18.6451 1.85446 18.7492 2.24961 18.7493H18.7452C19.1404 18.7492 19.5285 18.6451 19.8707 18.4474C20.2128 18.2497 20.4968 17.9654 20.6942 17.623C20.8915 17.2807 20.9952 16.8924 20.9948 16.4973C20.9944 16.1021 20.89 15.714 20.692 15.3721ZM9.74668 7.5C9.74668 7.30109 9.8257 7.11032 9.96635 6.96967C10.107 6.82902 10.2978 6.75 10.4967 6.75C10.6956 6.75 10.8864 6.82902 11.027 6.96967C11.1677 7.11032 11.2467 7.30109 11.2467 7.5V11.25C11.2467 11.4489 11.1677 11.6397 11.027 11.7803C10.8864 11.921 10.6956 12 10.4967 12C10.2978 12 10.107 11.921 9.96635 11.7803C9.8257 11.6397 9.74668 11.4489 9.74668 11.25V7.5ZM10.4972 15.7503C10.2747 15.7503 10.0572 15.6843 9.87222 15.5607C9.68721 15.4371 9.54302 15.2614 9.45787 15.0558C9.37272 14.8502 9.35044 14.624 9.39385 14.4058C9.43726 14.1876 9.5444 13.9871 9.70174 13.8298C9.85907 13.6724 10.0595 13.5653 10.2778 13.5219C10.496 13.4785 10.7222 13.5008 10.9278 13.5859C11.1333 13.6711 11.309 13.8152 11.4326 14.0003C11.5563 14.1853 11.6222 14.4028 11.6222 14.6253C11.6222 14.9236 11.5037 15.2098 11.2927 15.4207C11.0817 15.6317 10.7956 15.7503 10.4972 15.7503Z" fill="#FA5659"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75001 0C7.82164 0 5.93657 0.571828 4.33319 1.64317C2.72982 2.71451 1.48013 4.23726 0.742179 6.01884C0.00422452 7.80042 -0.188858 9.76081 0.187348 11.6521C0.563554 13.5434 1.49215 15.2807 2.85571 16.6443C4.21928 18.0079 5.95656 18.9365 7.84787 19.3127C9.73919 19.6889 11.6996 19.4958 13.4812 18.7578C15.2627 18.0199 16.7855 16.7702 17.8568 15.1668C18.9282 13.5634 19.5 11.6784 19.5 9.75C19.4971 7.16504 18.4689 4.68679 16.6411 2.85894C14.8132 1.03109 12.335 0.00292573 9.75001 0ZM9.56256 4.50003C9.78506 4.50003 10.0026 4.56601 10.1876 4.68963C10.3726 4.81325 10.5168 4.98895 10.6019 5.19452C10.6871 5.40008 10.7093 5.62628 10.6659 5.84451C10.6225 6.06274 10.5154 6.2632 10.3581 6.42053C10.2007 6.57786 10.0003 6.68501 9.78203 6.72842C9.5638 6.77183 9.3376 6.74955 9.13204 6.6644C8.92647 6.57925 8.75077 6.43506 8.62715 6.25005C8.50354 6.06505 8.43756 5.84754 8.43756 5.62503C8.43756 5.32667 8.55608 5.04052 8.76706 4.82954C8.97804 4.61856 9.26419 4.50003 9.56256 4.50003ZM10.5 15H9.75001C9.5511 15 9.36034 14.921 9.21969 14.7803C9.07904 14.6397 9.00002 14.4489 9.00001 14.25V9.75C8.8011 9.75 8.61033 9.67098 8.46968 9.53033C8.32903 9.38968 8.25001 9.19891 8.25001 9C8.25001 8.80109 8.32903 8.61032 8.46968 8.46967C8.61033 8.32902 8.8011 8.25 9.00001 8.25H9.75001C9.94892 8.25001 10.1397 8.32903 10.2803 8.46968C10.421 8.61033 10.5 8.80109 10.5 9V13.5C10.6989 13.5 10.8897 13.579 11.0303 13.7197C11.171 13.8603 11.25 14.0511 11.25 14.25C11.25 14.4489 11.171 14.6397 11.0303 14.7803C10.8897 14.921 10.6989 15 10.5 15Z" fill="#368CFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 0C7.82164 0 5.93657 0.571828 4.33319 1.64317C2.72982 2.71451 1.48013 4.23726 0.742179 6.01884C0.00422452 7.80042 -0.188858 9.76082 0.187348 11.6521C0.563554 13.5434 1.49215 15.2807 2.85571 16.6443C4.21928 18.0079 5.95656 18.9365 7.84787 19.3127C9.73919 19.6889 11.6996 19.4958 13.4812 18.7578C15.2627 18.0199 16.7855 16.7702 17.8568 15.1668C18.9282 13.5634 19.5 11.6784 19.5 9.75C19.497 7.16506 18.4688 4.68684 16.641 2.85901C14.8132 1.03118 12.3349 0.00298763 9.75 0ZM14.3936 8.04272L8.89307 13.2927C8.75322 13.4258 8.56756 13.5 8.37452 13.5C8.18147 13.5 7.99582 13.4258 7.85596 13.2927L5.10645 10.6677C4.9628 10.5301 4.87965 10.3411 4.87525 10.1423C4.87084 9.94341 4.94555 9.75092 5.08297 9.6071C5.22038 9.46328 5.40927 9.37989 5.60813 9.37523C5.80699 9.37058 5.99957 9.44504 6.14356 9.58228L8.37452 11.7129L13.3565 6.95728C13.5004 6.82004 13.693 6.74558 13.8919 6.75023C14.0907 6.75489 14.2796 6.83828 14.417 6.9821C14.5545 7.12592 14.6292 7.31841 14.6248 7.51727C14.6204 7.71614 14.5372 7.90513 14.3936 8.04272Z" fill="#2DD379"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 0C7.82164 0 5.93657 0.571827 4.33319 1.64317C2.72982 2.71451 1.48013 4.23726 0.742179 6.01884C0.00422452 7.80042 -0.188858 9.76082 0.187348 11.6521C0.563554 13.5434 1.49215 15.2807 2.85571 16.6443C4.21928 18.0079 5.95656 18.9365 7.84787 19.3127C9.73919 19.6889 11.6996 19.4958 13.4812 18.7578C15.2627 18.0199 16.7855 16.7702 17.8568 15.1668C18.9282 13.5634 19.5 11.6784 19.5 9.75C19.4971 7.16503 18.4689 4.68678 16.6411 2.85893C14.8132 1.03108 12.335 0.0029178 9.75 0ZM9 5.25C9 5.05109 9.07902 4.86032 9.21968 4.71967C9.36033 4.57902 9.55109 4.5 9.75 4.5C9.94892 4.5 10.1397 4.57902 10.2803 4.71967C10.421 4.86032 10.5 5.05109 10.5 5.25V10.5C10.5 10.6989 10.421 10.8897 10.2803 11.0303C10.1397 11.171 9.94892 11.25 9.75 11.25C9.55109 11.25 9.36033 11.171 9.21968 11.0303C9.07902 10.8897 9 10.6989 9 10.5V5.25ZM9.75 15C9.5275 15 9.30999 14.934 9.12499 14.8104C8.93998 14.6868 8.79579 14.5111 8.71064 14.3055C8.62549 14.1 8.60321 13.8738 8.64662 13.6555C8.69003 13.4373 8.79718 13.2368 8.95451 13.0795C9.11184 12.9222 9.3123 12.815 9.53053 12.7716C9.74876 12.7282 9.97496 12.7505 10.1805 12.8356C10.3861 12.9208 10.5618 13.065 10.6854 13.25C10.809 13.435 10.875 13.6525 10.875 13.875C10.875 14.1734 10.7565 14.4595 10.5455 14.6705C10.3345 14.8815 10.0484 15 9.75 15Z" fill="#FFA314"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+352
View File
@@ -0,0 +1,352 @@
:root {
--uspi-bodybg: #1e1f22; /* body背景色 */
--uspi-inputbg: #18191B; /* 输入框背景色 */
--uspi-textcolor: #fff; /* 文字颜色 */
--uspi-unitcolor: #A6A6A6; /* 单位字体颜色 */
--uspi-bordercolor: #3a3a3a; /* 边框颜色 */
--uspi-borderradius: 4px; /* 边框圆角 */
--uspi-width: 320px; /* 宽度 */
--uspi-height: 32px; /* 高度 */
--uspi-theme: #00FFE6; /* 主题颜色 */
--uspi-label-width: 112px; /* 标签宽度 */
}
*{
box-sizing: border-box;
}
html {
width: 100%;
padding: 0;
margin: 0;
min-height: 100vh;
}
html,body{
font-family: 'Source Han Sans SC', system-ui, -apple-system, Segoe UI, Roboto, Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
font-size: 14px;
line-height: 20px;
color: var(--uspi-textcolor);
}
body {
min-height: 100%;
padding: 0;
margin: 0;
}
a{
color: var(--uspi-theme);
cursor: pointer;
}
input,
textarea {
color: var(--uspi-textcolor); /* 改变可编辑区域内文字的颜色 */
caret-color: #4AA3FF; /* 改变可编辑区域光标的颜色 */
}
input::placeholder,
textarea::placeholder,
select::placeholder {
color: #65686D;
}
button:focus,
textarea:focus,
input:focus,
select:focus,
option:focus,
details:focus,
summary:focus{
outline: none;
}
.uspi-item{
display: flex;
align-items: center;
margin: 10px;
}
.uspi-item-label{
width: var(--uspi-label-width);
color: var(--uspi-textcolor);
text-align: right;
margin-right: 10px;
}
.uspi-item-label:after {
content: ": ";
}
.uspi-item-label.empty:after {
content: "";
}
.uspi-item-value{
width: var(--uspi-width);
display: flex;
align-items: center;
justify-content: space-between;
}
.uspi-item-value.no-label{
width: auto;
margin-left: var(--uspi-label-width);
display: inline-block;
}
select.uspi-item-value{
padding: 0 6px;
color: var(--uspi-textcolor);
height: var(--uspi-height);
background-color: var(--uspi-inputbg);
border: 1px solid var(--uspi-inputbg);
border-radius: var(--uspi-borderradius);
line-height: var(--uspi-height);
}
input.uspi-item-value{
padding: 0 10px;
height: var(--uspi-height);
background-color: var(--uspi-inputbg);
border: 1px solid var(--uspi-inputbg);
border-radius: var(--uspi-borderradius);
}
textarea.uspi-item-value {
resize: none;
height: 68px;
max-height: 132px;
background-color: var(--uspi-inputbg);
border: 1px solid var(--uspi-inputbg);
border-radius: var(--uspi-borderradius);
padding: 8px;
color: var(--uspi-textcolor);
}
[type="file"] .uspi-item-value{
/* padding: 0; */
height: var(--uspi-height);
background-color: var(--uspi-inputbg);
border: 1px solid var(--uspi-inputbg);
border-radius: var(--uspi-borderradius);
}
[type="file"] .uspi-file-info{
flex: 1;
padding: 0 10px;
color: var(--uspi-textcolor);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-color: var(--uspi-inputbg);
border: 1px solid var(--uspi-inputbg);
}
[type="file"] .uspi-file-label{
display: flex;
justify-content: center;
cursor: pointer;
margin-right: 8px;
}
input[type="radio"],
input[type="checkbox"]{
display: none;
}
input[type="radio"]+label,
input[type="checkbox"]+label{
padding-left: 24px;
background: url(../assets/u_check_none.svg) no-repeat left center;
color: var(--uspi-textcolor);
}
input[type="radio"]:checked+label{
background: url(../assets/u_check_radio.svg) no-repeat left center;
}
input[type="checkbox"]:checked+label {
background: url(../assets/u_check_checkbox.svg) no-repeat left center;
}
input[type="range"]{
flex: 1;
height: 4px;
background: var(--uspi-inputbg);
border-radius: 8px;
outline: none;
}
input[type="range"]+span{
text-align: right;
min-width: 25px;
padding-left: 8px;
}
.uspi-heading {
display: flex;
flex-basis: 100%;
align-items: center;
color: inherit;
font-size: 14px;
margin: 8px 0px;
}
.uspi-heading::before,
.uspi-heading::after {
content: "";
flex-grow: 1;
background: var(--uspi-bordercolor);
height: 1px;
font-size: 0px;
line-height: 0px;
margin: 0px 16px;
}
button{
cursor: pointer;
padding: 0 16px;
height: var(--uspi-height);
background: none;
border: 1px solid var(--uspi-theme);
border-radius: 8px;
color: var(--uspi-theme);
width: 124px;
}
button.primary{
background-color: var(--uspi-theme);
border: 1px solid var(--uspi-theme);
color: var(--uspi-bodybg);
}
button.uspi-item-value{
display: inline-block;
margin-left: 108px;
width: 124px;
}
button.default{
border: 1px solid #fff;
background-color: #fff;
color: var(--uspi-bodybg);
}
button.default-border{
border: 1px solid #fff;
color: #fff;
}
button.disabled{
cursor: not-allowed;
/* opacity: 0.6; */
border: 1px solid #65686D;
color: #65686D;
}
hr{
margin: 12px 16px;
border-style: none;
background: var(--uspi-bordercolor);
height: 1px;
}
.tip{
font-size: 12px;
color: var(--uspi-unitcolor);
margin: 0 10px;
line-height: 16px;
padding-left: 20px;
}
.tip.info{
background: url(../assets/u_tip_info.svg) no-repeat left center;
background-size: 16px 16px;
}
.tip.success{
background: url(../assets/u_tip_success.svg) no-repeat left center;
background-size: 16px 16px;
}
.tip.error{
background: url(../assets/u_tip_error.svg) no-repeat left center;
background-size: 16px 16px;
}
.tip.warn{
background: url(../assets/u_tip_warn.svg) no-repeat left center;
background-size: 16px 16px;
}
details{
color: var(--uspi-unitcolor);
}
.uspi-label-placeholder{
margin-left: var(--uspi-label-width);
}
.spinner {
width: 30px;
height: 30px;
border: 4px solid #555;
border-top: 4px solid rgba(255, 255, 255, 1);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 6px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.hidden{
display: none;
}
/* 滚动条整体样式 */
::-webkit-scrollbar {
width: 4px; /* 垂直滚动条宽度 */
height: 4px; /* 水平滚动条高度 */
}
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background: var(--uspi-bodybg); /* 轨道背景色 */
border-radius: 4px; /* 轨道圆角 */
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: #727476; /* 滑块背景色 */
border-radius: 4px; /* 滑块圆角 */
transition: background 0.3s; /* 过渡效果 */
}
/* 滚动条滑块悬停状态 */
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8; /* 悬停时的滑块颜色 */
}
/* 滚动条滑块激活状态(点击时) */
::-webkit-scrollbar-thumb:active {
background: #888888; /* 激活时的滑块颜色 */
}
/* 滚动条角落(垂直和水平滚动条交汇处) */
::-webkit-scrollbar-corner {
background: #f1f1f1; /* 角落背景色 */
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Events used for communicating with Ulanzi Stream Deck
*/
const Events = Object.freeze({
CONNECTED: 'connected',
CLOSE: 'close',
ERROR: 'error',
ADD: 'add',
RUN: 'run',
PARAMFROMAPP: 'paramfromapp',
PARAMFROMPLUGIN: 'paramfromplugin',
SETACTIVE: 'setactive',
CLEAR: 'clear',
TOAST:'toast',
STATE:'state',
OPENURL:'openurl',
OPENVIEW:'openview',
SELECTDIALOG:'selectdialog',
LOGMESSAGE:'logMessage',
HOTKEY:'hotkey',
SENDTOPROPERTYINSPECTOR:'sendToPropertyInspector',
SENDTOPLUGIN:'sendToPlugin',
SHOWALERT:'showAlert',
GETSETTINGS:'getSettings',
SETSETTINGS:'setSettings',
DIDRECEIVESETTINGS:'didReceiveSettings',
SETGLOBALSETTINGS:'setGlobalSettings',
GETGLOBALSETTINGS:'getGlobalSettings',
DIDRECEIVEGLOBALSETTINGS:'didReceiveGlobalSettings',
KEYDOWN:'keydown',
KEYUP:'keyup',
DIALEDOWN:'dialdown',
DIALEUP:'dialup',
DIALROTATE:'dialrotate'
});
/**
* Errors received from WebSocket
*/
const SocketErrors = {
DEFAULT:'closed *****'
};
+47
View File
@@ -0,0 +1,47 @@
class ULANZIEventEmitter {
constructor (id, debug = false) {
const eventList = new Map();
const ALLEVENTS = "*";
eventList.hasWildcard = function(name, data) {
for(const [key, value] of this) {
if(key !== ALLEVENTS && key.includes(ALLEVENTS) && new RegExp(`^${key.split(/\*+/).join('.*')}$`).test(name)) {
if(data) value.pub(data, name);
else return true;
}
}
};
this.on = (name, fn) => {
if(!eventList.has(name)) eventList.set(name, ULANZIEventEmitter.pubSub());
return eventList.get(name).sub(fn);
};
this.has = name => eventList.has(name);
this.hasMatch = name => eventList.has(name) || eventList.hasWildcard(name);
this.emit = (name, data) => {
eventList.has(name) && eventList.get(name).pub(data, name);
eventList.has(ALLEVENTS) && eventList.get(ALLEVENTS).pub(data, name);
eventList.hasWildcard(name, data);
};
return this;
}
static pubSub() {
const subscribers = new Set();
const sub = fn => {
subscribers.add(fn);
return () => {
subscribers.delete(fn);
};
};
const pub = (data, name) => subscribers.forEach(fn => fn(data, name));
return Object.freeze({pub, sub});
}
}
const EventEmitter = new ULANZIEventEmitter();
+87
View File
@@ -0,0 +1,87 @@
/* global USDTimerWorker */
let USDTimerWorker = new Worker(URL.createObjectURL(
new Blob([timerFn.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '')], {type: 'text/javascript'})
));
USDTimerWorker.timerId = 1;
USDTimerWorker.timers = {};
const USDDefaultTimeouts = {
timeout: 0,
interval: 10
};
Object.freeze(USDDefaultTimeouts);
function _setTimer(callback, delay, type, params) {
const id = USDTimerWorker.timerId++;
USDTimerWorker.timers[id] = {callback, params};
USDTimerWorker.onmessage = (e) => {
if(USDTimerWorker.timers[e.data.id]) {
if(e.data.type === 'clearTimer') {
delete USDTimerWorker.timers[e.data.id];
} else {
const cb = USDTimerWorker.timers[e.data.id].callback;
if(cb && typeof cb === 'function') cb(...USDTimerWorker.timers[e.data.id].params);
}
}
};
USDTimerWorker.postMessage({type, id, delay});
return id;
}
function _setTimeoutUSD(...args) {
let [callback, delay = 0, ...params] = [...args];
return _setTimer(callback, delay, 'setTimeout', params);
}
function _setIntervalUSD(...args) {
let [callback, delay = 0, ...params] = [...args];
return _setTimer(callback, delay, 'setInterval', params);
}
function _clearTimeoutUSD(id) {
USDTimerWorker.postMessage({type: 'clearTimeout', id}); // USDTimerWorker.postMessage({type: 'clearInterval', id}); = same thing
delete USDTimerWorker.timers[id];
}
window.setTimeout = _setTimeoutUSD;
window.setInterval = _setIntervalUSD;
window.clearTimeout = _clearTimeoutUSD; //timeout and interval share the same timer-pool
window.clearInterval = _clearTimeoutUSD;
function timerFn() {
let timers = {};
let debug = false;
let supportedCommands = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
function log(e) {console.log('Worker-Info::Timers', timers);}
function clearTimerAndRemove(id) {
if(timers[id]) {
if(debug) console.log('clearTimerAndRemove', id, timers[id], timers);
clearTimeout(timers[id]);
delete timers[id];
postMessage({type: 'clearTimer', id: id});
if(debug) log();
}
}
onmessage = function(e) {
// first see, if we have a timer with this id and remove it
// this automatically fulfils clearTimeout and clearInterval
supportedCommands.includes(e.data.type) && timers[e.data.id] && clearTimerAndRemove(e.data.id);
if(e.data.type === 'setTimeout') {
timers[e.data.id] = setTimeout(() => {
postMessage({id: e.data.id});
clearTimerAndRemove(e.data.id); //cleaning up
}, Math.max(e.data.delay || 0));
} else if(e.data.type === 'setInterval') {
timers[e.data.id] = setInterval(() => {
postMessage({id: e.data.id});
}, Math.max(e.data.delay || USDDefaultTimeouts.interval));
}
};
}
+904
View File
@@ -0,0 +1,904 @@
/// <reference path="eventEmitter.js"/>
/// <reference path="utils.js"/>
class UlanziStreamDeck {
constructor() {
this.key = "";
this.uuid = "";
this.actionid = "";
this.websocket = null;
this.language = "en";
this.localization = null;
this.on = EventEmitter.on;
this.emit = EventEmitter.emit;
this.isMain = false;
}
connect(uuid) {
this.port = Utils.getQueryParams("port") || 3906;
this.address = Utils.getQueryParams("address") || "127.0.0.1";
this.actionid = Utils.getQueryParams("actionid") || "";
this.key = Utils.getQueryParams("key") || "";
this.language =
Utils.getQueryParams("language") || Utils.getLanguage() || "en";
this.language = Utils.adaptLanguage(this.language);
this.uuid = Utils.getQueryParams("uuid") || uuid;
this.controller = Utils.getQueryParams("controller") || "Keypad"; //Keypad 按键 ,Encoder 旋钮
this.device = Utils.getQueryParams("device") || "";
this.mode = Utils.getQueryParams("mode") || "";
if (this.mode == "simulate") {
document.documentElement.style.backgroundColor = "#1E1F22";
document.body.style.backgroundColor = "#1E1F22";
}
if (this.websocket) {
this.websocket.close();
this.websocket = null;
}
//判断是否为主服务,约定主服务 uuid 为4位,action应大于4位
const isMain = this.uuid.split(".").length == 4;
this.isMain = isMain;
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET CONNECT:${
this.uuid
}`
);
this.websocket = new WebSocket(`ws://${this.address}:${this.port}`);
this.websocket.onopen = () => {
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET OPEN:${
this.uuid
}`
);
const json = {
code: 0,
cmd: Events.CONNECTED,
actionid: this.actionid,
key: this.key,
uuid: this.uuid,
};
this.websocket.send(JSON.stringify(json));
this.emit(Events.CONNECTED, {});
//如果是主服务,则不进行本地化
if (!isMain) {
this.localizeUI();
}
};
this.websocket.onerror = (evt) => {
const error = `[ULANZIDECK] ${
this.isMain ? "MAIN" : "CLIENT"
} WEBSOCKET ERROR: ${evt}, ${evt.data}, ${SocketErrors["DEFAULT"]}`;
Utils.warn(error);
this.emit(Events.ERROR, error);
};
this.websocket.onclose = (evt) => {
Utils.warn(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET CLOSED:${
SocketErrors["DEFAULT"]
}`
);
this.emit(Events.CLOSE);
};
this.websocket.onmessage = (evt) => {
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET MESSGE `
);
const data = evt && evt.data ? JSON.parse(evt.data) : null;
Utils.log(
`[ULANZIDECK] ${
this.isMain ? "MAIN" : "CLIENT"
} WEBSOCKET MESSGE DATA:${JSON.stringify(data)}`
);
//没有数据或者有data.code属性,且cmdType不等于REQUEST,则返回
if (
!data ||
(typeof data.code !== "undefined" && data.cmdType !== "REQUEST")
)
return;
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET MESSGE IN`
);
//没有key时,保存key
if (!this.key && data.uuid == this.uuid && data.key) {
this.key = data.key;
}
//没有actionid时,保存actionid
if (!this.actionid && data.uuid == this.uuid && data.actionid) {
this.actionid = data.actionid;
}
if (isMain) {
//主服务回应上位机
this.send(data.cmd, {
code: 0,
...data,
});
}
//特殊处理clear,因为clear事件变量是数组形式
if (data.cmd == "clear") {
if (data.param) {
for (let i = 0; i < data.param.length; i++) {
const context = this.encodeContext(data.param[i]);
data.param[i].context = context;
}
}
} else {
//拼接唯一id给功能页
const context = this.encodeContext(data);
data.context = context;
}
//引发事件
this.emit(data.cmd, data);
};
}
/**
* 本地化
*/
async localizeUI() {
const el = document.querySelector(".uspi-wrapper");
if (!el) return Utils.warn("No element found to localize");
// this.language = Utils.getLanguage() || 'en';
if (!this.localization) {
try {
const localJson = await Utils.readJson(
`${Utils.getPluginPath()}/${this.language}.json`
);
this.localization = localJson["Localization"]
? localJson["Localization"]
: null;
} catch (e) {
Utils.log(`${Utils.getPluginPath()}/${this.language}.json`);
Utils.warn(`No FILE found to localize: ${this.language}`);
}
}
if (this.localization){
const selectorsList = "[data-localize]";
el.querySelectorAll(selectorsList).forEach((e) => {
const s = e.innerText.trim();
let dl = e.dataset.localize;
if (e.placeholder && e.placeholder.length) {
// console.log('e.placeholder:',e.placeholder)
e.placeholder =
this.localization[dl ? dl : e.placeholder] || e.placeholder;
}
if (e.title && e.title.length) {
// console.log('e.title:',e.title)
e.title = this.localization[dl ? dl : e.title] || e.title;
}
if (e.label) {
// console.log('e.label:',e.label)
e.label = this.localization[dl ? dl : e.label] || e.label;
}
if (e.textContent) {
// console.log('e.textContent:',e.textContent)
e.textContent = this.localization[dl ? dl : e.textContent] || e.textContent;
}
if (s) {
// console.log('s:',s)
e.innerHTML = this.localization[dl ? dl : s] || e.innerHTML;
}
});
}
}
t(key) {
return (this.localization && this.localization[key]) || key;
}
/**
* 创建唯一值
*/
encodeContext(jsn) {
return jsn.uuid + "___" + jsn.key + "___" + jsn.actionid;
}
/**
* 解构唯一值
*/
decodeContext(context) {
const de_ctx = context.split("___");
return {
uuid: de_ctx[0],
key: de_ctx[1],
actionid: de_ctx[2],
};
}
/**
* Send JSON params to StreamDeck
* @param {string} cmd
* @param {object} params
*/
send(cmd, params) {
this.websocket &&
this.websocket.send(
JSON.stringify({
cmd,
uuid: this.uuid,
key: this.key,
actionid: this.actionid,
...params,
})
);
}
/**
* 向上位机发送配置参数
* @param {object} settings 必传 | 配置参数
* @param {object} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
sendParamFromPlugin(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.PARAMFROMPLUGIN, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
param: settings,
});
}
/**
* 请求上位机使⽤浏览器打开url
* @param {string} url 必传 | 直接远程地址和本地地址,⽀持打开插件根⽬录下的url链接(以/ ./ 起始的链接)。
* 只能是基本路径,不能带参数,需要带参数请设置在param值里面
* @param {local} boolean 可选 | 若为本地地址为true
* @param {object} param 可选 | 路径的参数值
*/
openUrl(url, local, param) {
this.send(Events.OPENURL, {
url,
local: local ? true : false,
param: param ? param : null,
});
}
/**
* 请求上位机机显⽰弹窗;弹窗后,test.html需要主动关闭,测试到window.close()可以通知弹窗关闭
* @param {string} url 必传 | 本地html路径,只能是基本路径,不能带参数,需要带参数请设置在param值里面
* @param {string} width 可选 | 窗口宽度,默认200
* @param {string} height 可选 | 窗口高度,默认200
* @param {string} x 可选 | 窗口x坐标,不传值默认居中
* @param {string} y 可选 | 窗口y坐标,不传值默认居中
* @param {object} param 可选 | 路径的参数值
*/
openView(url, width = 200, height = 200, x, y, param) {
const params = {
url,
width,
height,
};
if (x) {
params.x = x;
}
if (y) {
params.y = y;
}
if (param) {
params.param = param;
}
this.send(Events.OPENVIEW, params);
}
/**
* 请求上位机弹出Toast消息提⽰
* @param {string} msg 必传 | 窗口级消息提示
*/
toast(msg) {
this.send(Events.TOAST, {
msg,
});
}
/**
* 请求上位机弹出快捷键
* @param {string} key 必传 | 快捷键
*/
hotkey(key) {
this.send(Events.HOTKEY, {
keylist: key,
});
}
/**
* 请求上位机弹出日志消息提⽰
* @param {string} msg 必传 | 保存到插件UUID.txt中
* @param {string} level 可选 | 日志级别 info|debug|warn|error
*/
logMessage(msg, level) {
this.send(Events.LOGMESSAGE, {
message: msg,
level: level || "info",
});
}
/**
* 主服务发出,上位机透传参数到action页面,此透传参数上位机不保存
* @param {object} settings 必传 | 设置
* @param {string} context 必传 | 唯一id,需要指定发送到哪个action
*/
sendToPropertyInspector(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SENDTOPROPERTYINSPECTOR, {
uuid: uuid,
key: key,
actionid: actionid,
payload: settings,
});
}
/**
* action页面发出,上位机透传参数到主服务,此透传参数上位机不保存
* @param {object} settings 必传 | 设置
*/
sendToPlugin(settings) {
this.send(Events.SENDTOPLUGIN, {
uuid: this.uuid,
key: this.key,
actionid: this.actionid,
payload: settings,
});
}
/**
* 请求上位机在按键上显示错误提示
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
showAlert(context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SHOWALERT, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
});
}
/**
* 请求上位机发送已保存的参数,上位机接收后会触发didReceiveSettings事件转发至另一端
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
getSettings(context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.GETSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
});
}
/**
* 主动向上位机保存参数,上位机接收后会触发didReceiveSettings事件转发至另一端
* @param {object} settings 必传 | 配置参数
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
setSettings(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SETSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
settings,
});
}
/**
* 请求上位机发送已保存的全局参数,上位机接收后会触发didReceiveGlobalSettings事件转发至另一端
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
getGlobalSettings(context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.GETGLOBALSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
});
}
/**
* 主动向上位机保存参数,上位机接收后会触发didReceiveGlobalSettings事件转发至另一端
* @param {object} settings 必传 | 配置参数
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
setGlobalSettings(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SETGLOBALSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
settings,
});
}
/**
* 请求上位机弹出选择对话框:选择文件
* @param {string} filter 可选 | 文件过滤器。筛选文件的类型,例如 "filter": "image(*.jpg *.png *.gif)" 或者 筛选文件 file(*.txt *.json) 等
* 该请求的选择结果请通过 onSelectdialog 事件接收
*/
selectFileDialog(filter) {
this.send(Events.SELECTDIALOG, {
type: "file",
filter,
});
}
/**
* 请求上位机弹出选择对话框:选择文件夹
* 该请求的选择结果请通过 onSelectdialog 事件接收
*/
selectFolderDialog() {
this.send(Events.SELECTDIALOG, {
type: "folder",
});
}
/**
* 设置图标-使⽤配置⾥的图标列表编号,请对照manifest.json
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {number} state 必传 | 图标列表编号,
* @param {string} text 可选 | icon是否显示文字
*/
setStateIcon(context, state, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 0,
state,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤⾃定义图标
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {string} data 必传 | base64格式的icon
* @param {string} text 可选 | icon是否显示文字
*/
setBaseDataIcon(context, data, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 1,
data,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤本地图片文件
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {string} path 必传 | 本地图片路径,⽀持打开插件根⽬录下的url链接(以/ ./ 起始的链接)
* @param {string} text 可选 | icon是否显示文字
*/
setPathIcon(context, path, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 2,
path,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤⾃定义的动图
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {string} gifdata 必传 | ⾃定义gif的base64编码数据
* @param {string} text 可选 | icon是否显示文字
*/
setGifDataIcon(context, gifdata, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 3,
gifdata,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤本地gif⽂件
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出,
* @param {string} gifdata 必传 | 本地gif图片路径,⽀持打开插件根⽬录下的url链接(以/ ./ 起始的链接)
* @param {string} text 可选 | icon是否显示文字
*/
setGifPathIcon(context, gifpath, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 4,
gifpath,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 监听socket连接事件
*/
onConnected(fn) {
if (!fn) {
Utils.error(
"A callback function for the connected event is required for onConnected."
);
}
this.on(Events.CONNECTED, (jsn) => fn(jsn));
return this;
}
/**
* 监听socket断开事件
*/
onClose(fn) {
if (!fn) {
Utils.error(
"A callback function for the close event is required for onClose."
);
}
this.on(Events.CLOSE, (jsn) => fn(jsn));
return this;
}
/**
* 监听socket错误事件
*/
onError(fn) {
if (!fn) {
Utils.error(
"A callback function for the error event is required for onError."
);
}
this.on(Events.ERROR, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:add
*/
onAdd(fn) {
if (!fn) {
Utils.error(
"A callback function for the add event is required for onAdd."
);
}
this.on(Events.ADD, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:paramfromapp
*/
onParamFromApp(fn) {
if (!fn) {
Utils.error(
"A callback function for the paramfromapp event is required for onParamFromApp."
);
}
this.on(Events.PARAMFROMAPP, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:paramfromplugin
*/
onParamFromPlugin(fn) {
if (!fn) {
Utils.error(
"A callback function for the paramfromplugin event is required for onParamFromPlugin."
);
}
this.on(Events.PARAMFROMPLUGIN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:run
*/
onRun(fn) {
if (!fn) {
Utils.error(
"A callback function for the run event is required for onRun."
);
}
this.on(Events.RUN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:setactive
*/
onSetActive(fn) {
if (!fn) {
Utils.error(
"A callback function for the setactive event is required for onSetActive."
);
}
this.on(Events.SETACTIVE, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:clear
*/
onClear(fn) {
if (!fn) {
Utils.error(
"A callback function for the clear event is required for onClear."
);
}
this.on(Events.CLEAR, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:返回选择弹窗结果
*/
onSelectdialog(fn) {
if (!fn) {
Utils.error(
"A callback function for the selectdialog event is required for onSelectdialog."
);
}
this.on(Events.SELECTDIALOG, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:didReceiveSettings, 接受上位机保存的参数
*/
onDidReceiveSettings(fn) {
if (!fn) {
Utils.error(
"A callback function for the didReceiveSettings event is required for onDidReceiveSettings."
);
}
this.on(Events.DIDRECEIVESETTINGS, (jsn) => fn(jsn));
return this;
}
/**
* didReceiveGlobalSettings, 接受全局设置的参数
*/
onDidReceiveGlobalSettings(fn) {
if (!fn) {
Utils.error(
"A callback function for the didReceiveGlobalSettings event is required for onDidReceiveGlobalSettings."
);
}
this.on(Events.DIDRECEIVEGLOBALSETTINGS, (jsn) => fn(jsn));
return this;
}
/**
*
* 接收 主服务发给功能页的透传参数事件
*/
onSendToPropertyInspector(fn) {
if (!fn) {
Utils.error(
"A callback function for the sendToPropertyInspector event is required for onSendToPropertyInspector."
);
}
this.on(Events.SENDTOPROPERTYINSPECTOR, (jsn) => fn(jsn));
return this;
}
/**
*
* 接收 功能页发给主服务的透传参数事件
*/
onSendToPlugin(fn) {
if (!fn) {
Utils.error(
"A callback function for the sendToPlugin event is required for onSendToPlugin."
);
}
this.on(Events.SENDTOPLUGIN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:keydown, 接收上位机按键按下事件
*/
onKeyDown(fn) {
if (!fn) {
Utils.error(
"A callback function for the keydown event is required for onKeyDown."
);
}
this.on(Events.KEYDOWN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:keyup, 接收上位机按键松开事件
*/
onKeyUp(fn) {
if (!fn) {
Utils.error(
"A callback function for the keyup event is required for onKeyUp."
);
}
this.on(Events.KEYUP, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:dialdown, 接收上位机旋钮按下事件
*/
onDialDown(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialdown event is required for onDialDown."
);
}
this.on(Events.DIALEDOWN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:dialup, 接收上位机旋钮松开事件
*/
onDialUp(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialup event is required for onDialUp."
);
}
this.on(Events.DIALEUP, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮向左旋转事件
*/
onDialRotateLeft(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate left event is required for onDialRotateLeft."
);
}
this.on(Events.DIALROTATE, (jsn) => {
if (jsn.rotateEvent === "left") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮向右旋转事件
*/
onDialRotateRight(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate right event is required for onDialRotateRight."
);
}
this.on(Events.DIALROTATE, (jsn) => {
if (jsn.rotateEvent === "right") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮按住向左旋转事件
*/
onDialRotateHoldLeft(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate hold-left event is required for onDialRotateHoldLeft."
);
}
this.on(Events.DIALROTATE, (jsn) => {
if (jsn.rotateEvent === "hold-left") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮按住向右旋转事件
*/
onDialRotateHoldRight(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate hold-right event is required for onDialRotateHoldRight."
);
}
this.on(Events.DIALROTATE, (jsn) => {
// 注意:原数据中有个拼写错误"hold—right",这里使用正确的连字符
if (jsn.rotateEvent === "hold-right") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮旋转事件
*/
onDialRotate(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate event is required for onDialRotate."
);
}
this.on(Events.DIALROTATE, (jsn) => fn(jsn));
return this;
}
}
const $UD = new UlanziStreamDeck();
+549
View File
@@ -0,0 +1,549 @@
class UlanziUtils {
/**
* 获取表单数据
* Returns the value from a form using the form controls name property
* @param {Element | string} form
* @returns
*/
getFormValue(form) {
if (typeof form === 'string') {
form = document.querySelector(form);
}
const elements = form ? form.elements : '';
if (!elements) {
console.error('Could not find form!');
}
const formData = new FormData(form);
let formValue = {};
formData.forEach((value, key) => {
if (!Reflect.has(formValue, key)) {
formValue[key] = value;
return;
}
if (!Array.isArray(formValue[key])) {
formValue[key] = [formValue[key]];
}
formValue[key].push(value);
});
return formValue;
}
/**
* 重载表单数据
* Sets the value of form controls using their name attribute and the jsn object key
* @param {*} jsn
* @param {Element | string} form
*/
setFormValue(jsn, form) {
if (!jsn) {
return;
}
if (typeof form === 'string') {
form = document.querySelector(form);
}
const elements = form ? form.elements : '';
if (!elements) {
console.error('Could not find form!');
}
Array.from(elements)
.filter((element) => element ? element.name : null)
.forEach((element) => {
const { name, type } = element;
const value = name in jsn ? jsn[name] : null;
const isCheckOrRadio = type === 'checkbox' || type === 'radio';
if (value === null) return;
if (isCheckOrRadio) {
const isSingle = value === element.value;
if (isSingle || (Array.isArray(value) && value.includes(element.value))) {
element.checked = true;
}
} else {
element.value = value ? value : '';
}
});
}
/**
* 延迟触发
* This provides a slight delay before processing rapid events
* @param {function} fn
* @param {number} wait - delay before processing function (recommended time 150ms)
* @returns
*/
debounce(fn, wait = 150) {
let timeoutId = null
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
fn.apply(null, args);
}, wait);
};
}
/**
* 返回url的查询参数
*/
getQueryParams(param) {
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get(param);
}
/**
* 获取浏览器语言
* Returns the user language
*/
getLanguage() {
let userLanguage = navigator.languages && navigator.languages.length ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
if (userLanguage == 'zh') {
userLanguage = 'zh_CN'
} else if (userLanguage.indexOf('zh-') >= 0) {
userLanguage = userLanguage.split('-').join('_')
} else if (userLanguage.indexOf('-') !== -1) {
userLanguage = userLanguage.replace(/-/g, '_');
}
return this.adaptLanguage(userLanguage);
}
/**
* 适配语言环境
*/
adaptLanguage(ln) {
let userLanguage = ln;
if (ln.indexOf('zh') == 0) {
if(ln.indexOf('CN') > -1){
userLanguage = 'zh_CN'
}else{
userLanguage = 'zh_HK'
}
} else if (ln.indexOf('en') == 0) {
userLanguage = 'en'
} else if (userLanguage.indexOf('-') !== -1) {
userLanguage = userLanguage.replace(/-/g, '_');
}
return userLanguage
}
/**
* JSON.parse优化
* parse json
* @param {string} jsonString
* @returns {object} json
*/
parseJson(jsonString) {
if (typeof jsonString === 'object') return jsonString;
try {
const o = JSON.parse(jsonString);
if (o && typeof o === 'object') {
return o;
}
} catch (e) { }
return false;
}
/**
* 读取json文件
* Reads a json file
* @param {string} path
* @returns {Promise<any>} json
*/
async readJson(path) {
if (!path) {
console.error('A path is required to readJson.');
}
return new Promise((resolve, reject) => {
try {
const req = new XMLHttpRequest();
req.onerror = reject;
req.overrideMimeType('application/json');
req.open('GET', path, true);
req.onreadystatechange = (response) => {
if (req.readyState === 4) {
const jsonString = response && response.target && response.target.response || '';
if (jsonString) {
try {
resolve(JSON.parse(jsonString));
} catch (e) {
reject();
}
} else {
reject();
}
}
};
req.send();
} catch (e) {
reject();
}
});
}
/**
* 完整图片转base64
* @param {string} url 图片地址
* @param {number} width canvas宽度,默认196
* @param {number} height canvas宽度,默认196
* @param {HTMLCanvasElement} inCanvas canvas元素,默认创建
* @param {boolean} returnCanvas 是否返回canvas,默认false。默认返回base64的图片路径,有些时候需要接着画布添加元素,所以我们添加这个变量
* @return { string | HTMLCanvasElement } 默认返回base64的图片路径,returnCanvas为true返回画布
*/
async drawImage(url, width = 196, height = 196, inCanvas, returnCanvas) {
const canvas = inCanvas && inCanvas instanceof HTMLCanvasElement ? inCanvas : document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imgData = await this.loadImagePromise(url)
if (imgData.status == 'ok') {
ctx.drawImage(imgData.img, 0, 0, canvas.width, canvas.height);
}
return returnCanvas ? canvas : canvas.toDataURL('image/png'); //需要是否需要返回画布或者直接返回base64
}
/**
* 裁剪图片转base64
* @param {string} url 图片地址
* @param {number} offsetX 裁剪x的位置
* @param {number} offsetY 裁剪y的位置
* @param {number} width canvas宽度,默认196
* @param {number} height canvas宽度,默认196
* @param {HTMLCanvasElement} inCanvas canvas元素,默认创建
* @param {boolean} returnCanvas 是否返回canvas,默认false。默认返回base64的图片路径,有些时候需要接着画布添加元素,所以我们添加这个变量
* @return { string | HTMLCanvasElement } 默认返回base64的图片路径,returnCanvas为true返回画布
*/
async cropImage(url, offsetX, offsetY, width = 196, height = 196, inCanvas, returnCanvas) {
const canvas = inCanvas && inCanvas instanceof HTMLCanvasElement ? inCanvas : document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
const imgData = await this.loadImagePromise(url)
if (imgData.status == 'ok') {
ctx.drawImage(imgData.img, offsetX, offsetY, width, height, 0, 0, canvas.width, canvas.height);
}
return returnCanvas ? canvas : canvas.toDataURL('image/png'); //需要是否需要返回画布或者直接返回base64
};
/**
* 获取图片数据
* @param {string} url 图片地址
* @return {object} {url, status: 'ok', img} or {url, status: 'error'}
*/
loadImagePromise(url) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => resolve({ url, status: 'ok', img });
img.onerror = () => resolve({ url, status: 'error' });
img.src = url;
});
}
getData(url, param) {
param = Object.assign(param || {}, Utils.joinTimestamp());
//若参数有数组,进行特殊拼接
url = url + '?' + Object.keys(param).map(e => {
let str = ''
//判断数组拼接
if (param[e] instanceof Array) {
str = param[e].map((item) => {
return `${e}=${item}`
}).join('&')
} else {
str = `${e}=${param[e]}`
}
return str
}).join('&');
// console.warn('=====getData url:', url)
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.timeout = 1500; // 设置超时时间为 5 秒
req.ontimeout = function () {
console.error('Request timed out');
};
req.onload = function () {
// console.warn('=====getData onload:')
if (req.status === 200) {
// console.warn('=====getData success:')
resolve(req.response);
} else {
// console.warn('=====getData not 200:')
reject(Error(req.statusText));
}
};
req.onerror = function () {
// console.warn('=====getData error:')
reject(Error('Network Error'));
};
req.open('GET', url, true);
req.send();
});
};
/**
* 获取接口数据
* @param {string} url 接口地址
* @param {object} param 接口参数
* @param {string} method 请求方式:GET/POST/PUT/DELETE
* @param {object} headers 请求头
*/
fetchData(url, param, method = 'GET', headers = {}) {
if (method.toUpperCase() === 'GET') {
param = Object.assign(param || {}, Utils.joinTimestamp());
const tag = url.indexOf('?') >= 0 ? '&':'?'
//若参数有数组,进行特殊拼接
url = url + tag + Object.keys(param).map(e => {
let str = ''
//判断数组拼接
if (param[e] instanceof Array) {
str = param[e].map((item) => {
return `${e}=${item}`
}).join('&')
} else {
str = `${e}=${param[e]}`
}
return str
}).join('&');
}
const opts = {
cache: 'no-cache',
headers,
method: method,
body: ['GET', 'HEAD'].includes(method)
? undefined
: param,
};
return new Promise(function (resolve, reject) {
Utils.fetchWithTimeout(url, opts)
.then(async (resp) => {
// console.warn('==fetch success:', url)
if (!resp) {
reject(new Error('No Resp'));
}
if (!resp.ok) {
const errData = await resp.json();
if (errData) {
reject(errData);
} else {
reject(new Error(`{${resp.status}: ${await resp.text()}}`));
}
} else {
resolve(await resp.json());
}
})
.catch((err) => {
// console.warn('==fetch error:', JSON.stringify(err))
reject(err);
})
});
}
/**
* 封装fetch请求,设置超时时间
*/
fetchWithTimeout(url, options = {}) {
const { timeout = 15000 } = options; // 设置默认超时时间为8000ms
// console.warn('====fetchWithTimeout timeout:', timeout)
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
// console.warn('==fetchWithTimeout:', url, JSON.stringify(options))
const response = fetch(url, {
...options,
signal: controller.signal
}).then((response) => {
// console.warn('==fetchWithTimeout success:', JSON.stringify(response))
clearTimeout(id);
return response;
}).catch((error) => {
// console.warn('==fetchWithTimeout error:', JSON.stringify(error))
clearTimeout(id);
throw error;
});
return response;
}
/**
* 获取随机时间戳
*/
joinTimestamp() {
const now = new Date().getTime();
return { _t: now };
}
//判断是否为文件类型
isFile(variable) {
return variable instanceof File;
}
/**
* 浏览器file转base64
*/
htmlFileToBase64(file) {
if (!this.isFile(file)) {
return Promise.reject(new Error('Not a file'));
}
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
drawText(text, stroke = "#fff", background = "#000", wh = 196, textLabel, inCanvas) {
// console.log('==drawText:', text, textLabel)
const canvas = inCanvas ? inCanvas : document.createElement('canvas');
const ctx = canvas.getContext('2d');
if(!inCanvas){
canvas.width = wh;
canvas.height = wh;
if (background == "transparent") {
ctx.clearRect(0, 0, canvas.width, canvas.height);
} else {
ctx.fillStyle = background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
const fSize = text.length > 8 ? 30 - text.length / 2 : 40;
ctx.strokeStyle = "#000";
ctx.lineWidth = 4;
ctx.fillStyle = stroke;
ctx.font = `bold ${fSize}px "Source Han Sans SC"`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.strokeText(text, ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.fillText(text, ctx.canvas.width / 2, ctx.canvas.height / 2 );
if(textLabel){
ctx.font = `bold 24px "Source Han Sans SC"`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.fillText(textLabel, 10, 20);
}
return canvas.toDataURL('image/png')
}
getProperty(obj, dotSeparatedKeys, defaultValue) {
if (arguments.length > 1 && typeof dotSeparatedKeys !== 'string') return undefined;
if (typeof obj !== 'undefined' && typeof dotSeparatedKeys === 'string') {
const pathArr = dotSeparatedKeys.split('.');
pathArr.forEach((key, idx, arr) => {
if (typeof key === 'string' && key.includes('[')) {
try {
// extract the array index as string
const pos = /\[([^)]+)\]/.exec(key)[1];
// get the index string length (i.e. '21'.length === 2)
const posLen = pos.length;
arr.splice(idx + 1, 0, Number(pos));
// keep the key (array name) without the index comprehension:
// (i.e. key without [] (string of length 2)
// and the length of the index (posLen))
arr[idx] = key.slice(0, -2 - posLen); // eslint-disable-line no-param-reassign
} catch (e) {
// do nothing
}
}
});
// eslint-disable-next-line no-param-reassign, no-confusing-arrow
obj = pathArr.reduce((o, key) => (o && o[key] !== 'undefined' ? o[key] : undefined), obj);
}
return obj === undefined ? defaultValue : obj;
};
getProp(jsn, str, defaultValue = {}, sep = '.') {
const arr = str.split(sep);
return arr.reduce((obj, key) => (obj && obj.hasOwnProperty(key) ? obj[key] : defaultValue), jsn);
};
/**
* 获取插件根目录路径
*/
getPluginPath(){
const currentFilePath = location.pathname;
let split_tag = '/'
if(currentFilePath.indexOf('\\') > -1){
split_tag = '\\'
}
const pathArr = currentFilePath.split(split_tag);
const idx = pathArr.findIndex(f => f.endsWith('ulanziPlugin'));
const __folderpath = `${pathArr.slice(0, idx + 1).join("/")}`;
return __folderpath;
}
/**
* Logs a message
* @param {any} msg
*/
log(...msg) {
console.warn(`[${new Date().toLocaleString('zh-CN', { hour12: false })}]`, ...msg);
// this.getQueryParams('debug') && console.log(`[${new Date().toLocaleString('zh-CN', {hour12: false})}]`, ...msg);
}
/**
* Logs a warning message
*/
warn(...msg) {
console.warn(`[${new Date().toLocaleString('zh-CN', { hour12: false })}]`, ...msg);
}
/**
* Logs an error message
*/
error(...msg) {
console.error(`[${new Date().toLocaleString('zh-CN', { hour12: false })}]`, ...msg);
}
}
const Utils = new UlanziUtils()
+59
View File
@@ -0,0 +1,59 @@
{
"Version": "1.0.0",
"Author": "mars3142",
"Name": "mars3142 Collection",
"Description": "A collection of plugins by mars3142.",
"Icon": "assets/icons/icon.svg",
"Category": "mars3142",
"CategoryIcon": "assets/icons/icon.svg",
"CodePath": "plugin/app.html",
"Type": "JavaScript",
"SupportedInMultiActions": false,
"PrivateAPI": true,
"UUID": "dev.mars3142.ulanzideck.collection",
"Actions": [
{
"Name": "Petrol Watch",
"Icon": "assets/icons/petrol.svg",
"PropertyInspectorPath": "property-inspector/petrol/inspector.html",
"state": 0,
"States": [
{
"Name": "Default",
"Image": "assets/icons/petrol.svg"
}
],
"Tooltip": "Monitors petrol prices from a configured URL",
"UUID": "dev.mars3142.ulanzideck.collection.petrol",
"SupportedInMultiActions": false
},
{
"Name": "Copilot Usage",
"Icon": "assets/icons/copilot.png",
"PropertyInspectorPath": "property-inspector/copilot/inspector.html",
"state": 0,
"States": [
{
"Name": "Default",
"Image": "assets/icons/copilot.png"
}
],
"Tooltip": "Displays GitHub Copilot usage percentage as a gauge",
"UUID": "dev.mars3142.ulanzideck.collection.copilot",
"SupportedInMultiActions": false
}
],
"OS": [
{
"Platform": "mac",
"MinimumVersion": "10.11"
},
{
"Platform": "windows",
"MinimumVersion": "10"
}
],
"Software": {
"MinimumVersion": "2.1.18"
}
}
+175
View File
@@ -0,0 +1,175 @@
class CopilotAction {
constructor($UD, context) {
this.$UD = $UD;
this.context = context;
this.config = {
url: '',
refreshRate: '4',
};
this.refreshTimer = null;
this.debounceTimer = 0;
}
setActive() {}
onClear() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = 0;
}
}
setParams(jsn) {
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
this.start();
}
onRun() {
this.fetchData();
}
start() {
this.onClear();
this.fetchData();
const duration = this.getRefreshDuration();
if (duration > 0) {
this.refreshTimer = setInterval(() => this.fetchData(), duration * 60 * 1000);
}
}
async fetchData() {
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(async () => {
if (!this.config.url) {
this.renderGauge(null);
return;
}
try {
const response = await fetch(this.config.url);
const text = await response.text();
const value = this.parsePrometheus(text);
this.renderGauge(value !== null ? parseFloat(value) : null);
} catch (e) {
this.renderGauge(null);
}
}, 300);
}
parsePrometheus(text) {
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (trimmed.startsWith('github_copilot_usage_percentage')) {
const closingBrace = trimmed.indexOf('}');
if (closingBrace !== -1) return trimmed.slice(closingBrace + 1).trim();
// no labels
const parts = trimmed.split(/\s+/);
if (parts.length >= 2) return parts[1];
}
}
return null;
}
renderGauge(value) {
const canvas = document.createElement('canvas');
canvas.width = 196;
canvas.height = 196;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, 196, 196);
const cx = 98, cy = 100;
const r = 68;
const lineWidth = 14;
// Gauge runs from 225° to 315° clockwise (270° sweep)
const startAngle = 135 * Math.PI / 180;
const totalSweep = 270 * Math.PI / 180;
const greenEnd = startAngle + 0.50 * totalSweep; // 50%
const yellowEnd = startAngle + 0.80 * totalSweep; // 80%
const endAngle = startAngle + totalSweep; // 100%
// Background track
ctx.beginPath();
ctx.arc(cx, cy, r, startAngle, endAngle, false);
ctx.strokeStyle = '#333333';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();
if (value !== null) {
const pct = Math.max(0, Math.min(100, value)) / 100;
const valueAngle = startAngle + pct * totalSweep;
// Green segment (050%)
if (pct > 0) {
ctx.beginPath();
ctx.arc(cx, cy, r, startAngle, Math.min(valueAngle, greenEnd), false);
ctx.strokeStyle = '#4caf50';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();
}
// Yellow segment (5080%)
if (pct > 0.5) {
ctx.beginPath();
ctx.arc(cx, cy, r, greenEnd, Math.min(valueAngle, yellowEnd), false);
ctx.strokeStyle = '#ffeb3b';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'butt';
ctx.stroke();
}
// Red segment (80100%)
if (pct > 0.8) {
ctx.beginPath();
ctx.arc(cx, cy, r, yellowEnd, valueAngle, false);
ctx.strokeStyle = '#f44336';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'butt';
ctx.stroke();
}
// Value text — centered, colored
const color = pct <= 0.5 ? '#4caf50' : pct <= 0.8 ? '#ffeb3b' : '#f44336';
const displayText = value.toFixed(1) + '%';
const fSize = displayText.length > 6 ? 28 : 34;
ctx.fillStyle = color;
ctx.font = `bold ${fSize}px "Source Han Sans SC"`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(displayText, cx, cy + 8);
} else {
ctx.fillStyle = '#666666';
ctx.font = 'bold 28px "Source Han Sans SC"';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText('N/A', cx, cy + 8);
}
// Label — bottom
ctx.fillStyle = '#7ec8e3';
ctx.font = 'bold 20px "Source Han Sans SC"';
ctx.fillText('Copilot', cx, 165);
this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png'));
}
getRefreshDuration() {
switch (this.config.refreshRate) {
case '1': return 1;
case '2': return 2;
case '3': return 5;
case '4': return 10;
case '5': return 30;
case '6': return 60;
default: return 0;
}
}
}
+151
View File
@@ -0,0 +1,151 @@
class PetrolAction {
constructor($UD, context) {
this.$UD = $UD;
this.context = context;
this.config = {
url: '',
stationUuid: '',
fuelType: '',
refreshRate: '1',
};
this.refreshTimer = null;
this.debounceTimer = 0;
this.previousPrice = null;
this.lastDelta = null;
this.$UD.onDidReceiveSettings(jsn => {
if (jsn.context !== this.context) return;
const s = jsn.settings || {};
this.config = Object.assign(this.config, s);
if (s.previousPrice != null) this.previousPrice = s.previousPrice;
if (s.lastDelta != null) this.lastDelta = s.lastDelta;
});
}
setActive() {}
onClear() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = 0;
}
}
setParams(jsn) {
const params = (jsn && jsn.param) || {};
this.config = Object.assign(this.config, params);
this.$UD.getSettings(this.context);
this.start();
}
onRun() {
this.fetchPrice();
}
start() {
this.onClear();
this.fetchPrice();
const duration = this.getRefreshDuration();
if (duration > 0) {
this.refreshTimer = setInterval(() => this.fetchPrice(), duration * 60 * 1000);
}
}
async fetchPrice() {
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(async () => {
if (!this.config.url || !this.config.stationUuid || !this.config.fuelType) {
this.renderButton('?', null);
return;
}
try {
const response = await fetch(this.config.url);
const text = await response.text();
const raw = this.parsePrometheus(text);
if (raw) {
const current = parseFloat(raw);
if (this.previousPrice !== null) {
const delta = current - this.previousPrice;
if (delta !== 0) this.lastDelta = delta;
}
this.previousPrice = current;
this.renderButton(`${raw}`, this.lastDelta);
this.$UD.setSettings({ ...this.config, previousPrice: this.previousPrice, lastDelta: this.lastDelta }, this.context);
} else {
this.renderButton('N/A', null);
}
} catch (e) {
this.renderButton('ERR', null);
}
}, 300);
}
parsePrometheus(text) {
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (trimmed.startsWith('petrol_price_euro') && trimmed.includes(this.config.stationUuid) && trimmed.includes(this.config.fuelType)) {
const closingBrace = trimmed.indexOf('}');
if (closingBrace !== -1) return trimmed.slice(closingBrace + 1).trim();
}
}
return null;
}
renderButton(priceText, delta) {
const canvas = document.createElement('canvas');
canvas.width = 196;
canvas.height = 196;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, 196, 196);
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
// Fuel type — top, light blue
ctx.fillStyle = '#7ec8e3';
ctx.font = 'bold 28px "Source Han Sans SC"';
ctx.strokeText(this.config.fuelType, 98, 45);
ctx.fillText(this.config.fuelType, 98, 45);
// Price — middle, yellow
const fSize = priceText.length > 6 ? 34 - priceText.length : 40;
ctx.fillStyle = '#f0c040';
ctx.font = `bold ${fSize}px "Source Han Sans SC"`;
ctx.strokeText(priceText, 98, 105);
ctx.fillText(priceText, 98, 105);
// Delta — bottom
if (delta !== null) {
const arrow = delta > 0 ? '▲' : '▼';
const color = delta > 0 ? '#ff6b6b' : '#6bff6b';
const deltaText = `${arrow} ${Math.abs(delta).toFixed(3)}`;
ctx.fillStyle = color;
ctx.font = '18px "Source Han Sans SC"';
ctx.strokeText(deltaText, 98, 142);
ctx.fillText(deltaText, 98, 142);
}
this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png'));
}
getRefreshDuration() {
switch (this.config.refreshRate) {
case '1': return 1;
case '2': return 2;
case '3': return 5;
case '4': return 10;
case '5': return 30;
case '6': return 60;
default: return 0;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>mars3142 Collection</title>
</head>
<body>
<script src="../libs/js/constants.js"></script>
<script src="../libs/js/eventEmitter.js"></script>
<script src="../libs/js/timers.js"></script>
<script src="../libs/js/utils.js"></script>
<script src="../libs/js/ulanzideckApi.js"></script>
<script src="./actions/PetrolAction.js"></script>
<script src="./actions/CopilotAction.js"></script>
<script src="./app.js"></script>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
const ACTION_CACHES = {};
$UD.connect('dev.mars3142.ulanzideck.collection');
$UD.onConnected(() => {
console.log('app.js onConnected');
});
$UD.onAdd(jsn => {
const context = jsn.context;
console.log('app.js onAdd:', JSON.stringify(jsn));
if (!ACTION_CACHES[context]) {
const name = jsn.uuid.split('.').pop().toLowerCase();
if (name === 'petrol') {
ACTION_CACHES[context] = new PetrolAction($UD, context);
} else if (name === 'copilot') {
ACTION_CACHES[context] = new CopilotAction($UD, context);
}
}
if (ACTION_CACHES[context]) ACTION_CACHES[context].setParams(jsn);
});
$UD.onSetActive(jsn => {
const instance = ACTION_CACHES[jsn.context];
if (instance) instance.setActive(jsn.active);
});
$UD.onRun(jsn => {
const context = jsn.context;
if (!ACTION_CACHES[context]) {
$UD.emit('add', jsn);
} else {
ACTION_CACHES[context].onRun();
}
});
$UD.onClear(jsn => {
if (jsn.param) {
for (let i = 0; i < jsn.param.length; i++) {
const context = jsn.param[i].context;
if (ACTION_CACHES[context]) {
ACTION_CACHES[context].onClear();
delete ACTION_CACHES[context];
}
}
}
});
$UD.onParamFromApp(jsn => {
console.log('app.js onParamFromApp:', JSON.stringify(jsn.param));
onSetSettings(jsn);
});
$UD.onParamFromPlugin(jsn => {
console.log('app.js onParamFromPlugin:', JSON.stringify(jsn.param));
onSetSettings(jsn);
});
function onSetSettings(jsn) {
const action = ACTION_CACHES[jsn.context];
if (!action) return;
action.setParams(jsn);
}
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>Copilot Usage</title>
<link rel="stylesheet" href="../../libs/css/uspi.css">
</head>
<body>
<div class="uspi-wrapper">
<form id="property-inspector">
<div class="uspi-item">
<div class="uspi-item-label" data-localize>URL</div>
<input class="uspi-item-value" type="text" name="url" placeholder="https://..." />
</div>
<div class="uspi-item">
<div class="uspi-item-label" data-localize>Refresh Rate</div>
<select class="uspi-item-value" name="refreshRate">
<option value="0" data-localize>On Press</option>
<option value="1" data-localize>Every 1 min</option>
<option value="2" data-localize>Every 2 min</option>
<option value="3" data-localize>Every 5 min</option>
<option value="4" selected data-localize>Every 10 min</option>
<option value="5" data-localize>Every 30 min</option>
<option value="6" data-localize>Every Hour</option>
</select>
</div>
</form>
</div>
<script src="../../libs/js/constants.js"></script>
<script src="../../libs/js/eventEmitter.js"></script>
<script src="../../libs/js/utils.js"></script>
<script src="../../libs/js/ulanzideckApi.js"></script>
<script src="./inspector.js"></script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
let ACTION_SETTING = {};
let form = '';
$UD.connect('dev.mars3142.ulanzideck.collection.copilot');
$UD.onConnected(() => {
form = document.querySelector('#property-inspector');
form.addEventListener('input', Utils.debounce(() => {
const value = Utils.getFormValue(form);
ACTION_SETTING = { ...ACTION_SETTING, ...value };
$UD.sendParamFromPlugin(ACTION_SETTING);
}));
});
$UD.onAdd(jsn => {
if (jsn && jsn.param) settingSaveParam(jsn.param);
});
$UD.onParamFromApp(jsn => {
settingSaveParam((jsn && jsn.param) || {});
});
$UD.onParamFromPlugin(jsn => {
settingSaveParam((jsn && jsn.param) || {});
});
function settingSaveParam(params) {
ACTION_SETTING = { ...ACTION_SETTING, ...params };
if (form) Utils.setFormValue(ACTION_SETTING, form);
}
+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>Petrol Watch</title>
<link rel="stylesheet" href="../../libs/css/uspi.css">
</head>
<body>
<div class="uspi-wrapper">
<form id="property-inspector">
<div class="uspi-item">
<div class="uspi-item-label" data-localize>URL</div>
<input class="uspi-item-value" type="text" name="url" placeholder="https://..." />
</div>
<div class="uspi-item">
<div class="uspi-item-label" data-localize>Station UUID</div>
<input class="uspi-item-value" type="text" name="stationUuid" placeholder="xxxxxxxx-xxxx-..." />
</div>
<div class="uspi-item">
<div class="uspi-item-label" data-localize>Fuel Type</div>
<input class="uspi-item-value" type="text" name="fuelType" placeholder="Super E5" />
</div>
<div class="uspi-item">
<div class="uspi-item-label" data-localize>Refresh Rate</div>
<select class="uspi-item-value" name="refreshRate">
<option value="0" data-localize>On Press</option>
<option value="1" data-localize>Every 1 min</option>
<option value="2" data-localize>Every 2 min</option>
<option value="3" data-localize>Every 5 min</option>
<option value="4" selected data-localize>Every 10 min</option>
<option value="5" data-localize>Every 30 min</option>
<option value="6" data-localize>Every Hour</option>
</select>
</div>
</form>
</div>
<script src="../../libs/js/constants.js"></script>
<script src="../../libs/js/eventEmitter.js"></script>
<script src="../../libs/js/utils.js"></script>
<script src="../../libs/js/ulanzideckApi.js"></script>
<script src="./inspector.js"></script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
let ACTION_SETTING = {};
let form = '';
$UD.connect('dev.mars3142.ulanzideck.collection.petrol');
$UD.onConnected(() => {
form = document.querySelector('#property-inspector');
form.addEventListener('input', Utils.debounce(() => {
const value = Utils.getFormValue(form);
ACTION_SETTING = { ...ACTION_SETTING, ...value };
$UD.sendParamFromPlugin(ACTION_SETTING);
}));
});
$UD.onAdd(jsn => {
if (jsn && jsn.param) settingSaveParam(jsn.param);
});
$UD.onParamFromApp(jsn => {
settingSaveParam((jsn && jsn.param) || {});
});
$UD.onParamFromPlugin(jsn => {
settingSaveParam((jsn && jsn.param) || {});
});
function settingSaveParam(params) {
ACTION_SETTING = { ...ACTION_SETTING, ...params };
if (form) Utils.setFormValue(ACTION_SETTING, form);
}