diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9d8798e34..7d58530ac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -143,8 +143,6 @@ dependencies { implementation("com.github.Dimezis:BlurView:version-1.6.6") implementation("org.altbeacon:android-beacon-library:2.19.5") - implementation("com.maltaisn:icondialog:3.3.0") - implementation("com.maltaisn:iconpack-community-material:5.3.45") implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.22") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") diff --git a/app/src/main/assets/mdi_id_map.json b/app/src/main/assets/mdi_id_map.json new file mode 100644 index 000000000..64f730a63 --- /dev/null +++ b/app/src/main/assets/mdi_id_map.json @@ -0,0 +1 @@ +{"ab-testing":983068,"abjad-arabic":987944,"abjad-hebrew":987945,"abugida-devanagari":987946,"abugida-thai":987947,"access-point":61442,"access-point-network":61443,"access-point-network-off":64445,"account":61444,"account-alert":61445,"account-arrow-left":64301,"account-arrow-right":64303,"account-box":61446,"account-box-multiple":63795,"account-cancel":983818,"account-cash":983234,"account-check":61448,"account-child":64136,"account-child-circle":64137,"account-circle":61449,"account-clock":64306,"account-cog":988016,"account-convert":61450,"account-cowboy-hat":65208,"account-details":63025,"account-edit":63163,"account-eye":62496,"account-group":63560,"account-hard-hat":62901,"account-heart":63640,"account-key":61451,"account-lock":983433,"account-minus":61453,"account-multiple":61454,"account-multiple-check":63684,"account-multiple-minus":62931,"account-multiple-plus":61456,"account-multiple-remove":983605,"account-music":63490,"account-network":61457,"account-off":61458,"account-plus":61460,"account-question":64309,"account-remove":61461,"account-search":61462,"account-settings":63024,"account-star":61463,"account-supervisor":64138,"account-supervisor-circle":64139,"account-switch":61465,"account-tie":64703,"account-tie-voice":983859,"account-tie-voice-off":983861,"account-voice":62923,"account-voice-off":65265,"adjust":61466,"air-conditioner":61467,"air-filter":64799,"air-horn":64904,"air-humidifier":983236,"air-humidifier-off":988262,"air-purifier":64800,"airbag":64453,"airballoon":61468,"airplane":61469,"airplane-landing":62932,"airplane-off":61470,"airplane-takeoff":62933,"airport":63562,"alarm":61472,"alarm-bell":63373,"alarm-check":61473,"alarm-light":63374,"alarm-multiple":61474,"alarm-note":65166,"alarm-note-off":65167,"alarm-off":61475,"alarm-plus":61476,"alarm-snooze":63117,"album":61477,"alert":61478,"alert-box":61479,"alert-circle":61480,"alert-circle-check":983576,"alert-decagram":63164,"alert-minus":988347,"alert-octagon":61481,"alert-octagram":63334,"alert-plus":988346,"alert-remove":988348,"alert-rhombus":983545,"alien":63641,"align-horizontal-center":983534,"align-horizontal-left":983533,"align-horizontal-right":983535,"align-vertical-bottom":983536,"align-vertical-center":983537,"align-vertical-top":983538,"all-inclusive":63165,"allergy":983683,"alpha":61483,"alpha-a":65,"alpha-a-box":64237,"alpha-a-circle":64456,"alpha-b":66,"alpha-b-box":64238,"alpha-b-circle":64459,"alpha-c":67,"alpha-c-box":64239,"alpha-c-circle":64462,"alpha-d":68,"alpha-d-box":64240,"alpha-d-circle":64465,"alpha-e":69,"alpha-e-box":64241,"alpha-e-circle":64468,"alpha-f":70,"alpha-f-box":64242,"alpha-f-circle":64471,"alpha-g":71,"alpha-g-box":64243,"alpha-g-circle":64474,"alpha-h":72,"alpha-h-box":64244,"alpha-h-circle":64477,"alpha-i":73,"alpha-i-box":64245,"alpha-i-circle":64480,"alpha-j":74,"alpha-j-box":64246,"alpha-j-circle":64483,"alpha-k":75,"alpha-k-box":64247,"alpha-k-circle":64486,"alpha-l":76,"alpha-l-box":64248,"alpha-l-circle":64489,"alpha-m":77,"alpha-m-box":64249,"alpha-m-circle":64492,"alpha-n":78,"alpha-n-box":64250,"alpha-n-circle":64495,"alpha-o":79,"alpha-o-box":64251,"alpha-o-circle":64498,"alpha-p":80,"alpha-p-box":64252,"alpha-p-circle":64501,"alpha-q":81,"alpha-q-box":64253,"alpha-q-circle":64504,"alpha-r":82,"alpha-r-box":64254,"alpha-r-circle":64507,"alpha-s":83,"alpha-s-box":64255,"alpha-s-circle":64510,"alpha-t":84,"alpha-t-box":64256,"alpha-t-circle":64513,"alpha-u":85,"alpha-u-box":64257,"alpha-u-circle":64516,"alpha-v":86,"alpha-v-box":64258,"alpha-v-circle":64519,"alpha-w":87,"alpha-w-box":64259,"alpha-w-circle":64522,"alpha-x":88,"alpha-x-box":64260,"alpha-x-circle":64525,"alpha-y":89,"alpha-y-box":64261,"alpha-y-circle":64528,"alpha-z":90,"alpha-z-box":64262,"alpha-z-circle":64531,"alphabet-aurebesh":987948,"alphabet-cyrillic":987949,"alphabet-greek":987950,"alphabet-latin":987951,"alphabet-piqad":987952,"alphabet-tengwar":987959,"alphabetical":61484,"alphabetical-off":983086,"alphabetical-variant":983087,"alphabetical-variant-off":983088,"altimeter":62935,"ambulance":61487,"ammunition":64708,"ampersand":64140,"amplifier":61488,"amplifier-off":983520,"anchor":61489,"angle-acute":63798,"angle-obtuse":63799,"angle-right":63800,"animation":62936,"animation-play":63801,"antenna":983364,"anvil":63642,"api":983238,"api-off":983682,"apple-keyboard-caps":63026,"apple-keyboard-command":63027,"apple-keyboard-control":63028,"apple-keyboard-option":63029,"apple-keyboard-shift":63030,"application-array":983328,"application-braces":983330,"application-brackets":64615,"application-export":64905,"application-import":64906,"application-outline":62996,"application-parentheses":983332,"application-variable":983334,"approximately-equal":65470,"approximately-equal-box":65471,"apps":61499,"apps-box":64802,"archive":61500,"archive-arrow-down":983684,"archive-arrow-up":983686,"arm-flex":983183,"arrange-bring-forward":61501,"arrange-bring-to-front":61502,"arrange-send-backward":61503,"arrange-send-to-back":61504,"arrow-all":61505,"arrow-bottom-left":61506,"arrow-bottom-left-thick":63927,"arrow-bottom-right":61507,"arrow-bottom-right-thick":63929,"arrow-collapse":62997,"arrow-collapse-all":61508,"arrow-collapse-down":63377,"arrow-collapse-horizontal":63563,"arrow-collapse-left":63378,"arrow-collapse-right":63379,"arrow-collapse-up":63380,"arrow-collapse-vertical":63564,"arrow-decision":63930,"arrow-decision-auto":63931,"arrow-down":61509,"arrow-down-bold":63277,"arrow-down-bold-box":63278,"arrow-down-bold-circle":61511,"arrow-down-box":63167,"arrow-down-circle":64695,"arrow-down-drop-circle":61514,"arrow-down-thick":61510,"arrow-expand":62998,"arrow-expand-all":61516,"arrow-expand-down":63381,"arrow-expand-horizontal":63565,"arrow-expand-left":63382,"arrow-expand-right":63383,"arrow-expand-up":63384,"arrow-expand-vertical":63566,"arrow-horizontal-lock":983430,"arrow-left":61517,"arrow-left-bold":63280,"arrow-left-bold-box":63281,"arrow-left-bold-circle":61519,"arrow-left-box":63168,"arrow-left-circle":64697,"arrow-left-drop-circle":61522,"arrow-left-right":65168,"arrow-left-right-bold":65169,"arrow-left-thick":61518,"arrow-right":61524,"arrow-right-bold":63283,"arrow-right-bold-box":63284,"arrow-right-bold-circle":61526,"arrow-right-box":63169,"arrow-right-circle":64699,"arrow-right-drop-circle":61529,"arrow-right-thick":61525,"arrow-split-horizontal":63802,"arrow-split-vertical":63803,"arrow-top-left":61531,"arrow-top-left-bottom-right":65170,"arrow-top-left-bottom-right-bold":65171,"arrow-top-left-thick":63939,"arrow-top-right":61532,"arrow-top-right-bottom-left":65172,"arrow-top-right-bottom-left-bold":65173,"arrow-top-right-thick":63941,"arrow-up":61533,"arrow-up-bold":63286,"arrow-up-bold-box":63287,"arrow-up-bold-circle":61535,"arrow-up-box":63170,"arrow-up-circle":64701,"arrow-up-down":65174,"arrow-up-down-bold":65175,"arrow-up-drop-circle":61538,"arrow-up-thick":61534,"arrow-vertical-lock":983431,"aspect-ratio":64035,"assistant":61540,"asterisk":63171,"at":61541,"atm":64803,"atom":63335,"atom-variant":65176,"attachment":61542,"audio-video":63804,"audio-video-off":983521,"augmented-reality":63567,"auto-download":988030,"auto-fix":61544,"auto-upload":61545,"autorenew":61546,"av-timer":61547,"axe":63687,"axis":64804,"axis-arrow":64805,"axis-arrow-info":988174,"axis-arrow-lock":64806,"axis-lock":64807,"axis-x-arrow":64808,"axis-x-arrow-lock":64809,"axis-x-rotate-clockwise":64810,"axis-x-rotate-counterclockwise":64811,"axis-x-y-arrow-lock":64812,"axis-y-arrow":64813,"axis-y-arrow-lock":64814,"axis-y-rotate-clockwise":64815,"axis-y-rotate-counterclockwise":64816,"axis-z-arrow":64817,"axis-z-arrow-lock":64818,"axis-z-rotate-clockwise":64819,"axis-z-rotate-counterclockwise":64820,"baby":61548,"baby-bottle":65366,"baby-buggy":988128,"baby-carriage":63118,"baby-carriage-off":65472,"baby-face":65177,"backburger":61549,"backspace":61550,"backspace-reverse":65179,"backup-restore":61551,"bacteria":65266,"badge-account":64899,"badge-account-alert":64900,"badge-account-horizontal":65008,"badminton":63568,"bag-carry-on":65368,"bag-carry-on-check":64833,"bag-carry-on-off":65369,"bag-checked":65370,"bag-personal":65011,"bag-personal-off":65012,"baguette":65371,"balloon":64037,"ballot":63944,"ballot-recount":64533,"bandage":64907,"bank":61552,"bank-minus":64908,"bank-plus":64909,"bank-remove":64910,"bank-transfer":64038,"bank-transfer-in":64039,"bank-transfer-out":64040,"barcode":61553,"barcode-off":983649,"barcode-scan":61554,"barley":61555,"barley-off":64313,"barn":64314,"barrel":61556,"baseball":63569,"baseball-bat":63570,"bash":983470,"basket":61558,"basket-fill":61559,"basket-unfill":61560,"basketball":63493,"basketball-hoop":64535,"bat":64315,"battery":61561,"battery-10":61562,"battery-10-bluetooth":63805,"battery-20":61563,"battery-20-bluetooth":63806,"battery-30":61564,"battery-30-bluetooth":63807,"battery-40":61565,"battery-40-bluetooth":63808,"battery-50":61566,"battery-50-bluetooth":63809,"battery-60":61567,"battery-60-bluetooth":63810,"battery-70":61568,"battery-70-bluetooth":63811,"battery-80":61569,"battery-80-bluetooth":63812,"battery-90":61570,"battery-90-bluetooth":63813,"battery-alert":61571,"battery-alert-bluetooth":63814,"battery-alert-variant":983287,"battery-bluetooth":63815,"battery-bluetooth-variant":63816,"battery-charging":61572,"battery-charging-10":63643,"battery-charging-100":61573,"battery-charging-20":61574,"battery-charging-30":61575,"battery-charging-40":61576,"battery-charging-50":63644,"battery-charging-60":61577,"battery-charging-70":63645,"battery-charging-80":61578,"battery-charging-90":61579,"battery-charging-high":983761,"battery-charging-low":983759,"battery-charging-medium":983760,"battery-charging-wireless":63494,"battery-charging-wireless-10":63495,"battery-charging-wireless-20":63496,"battery-charging-wireless-30":63497,"battery-charging-wireless-40":63498,"battery-charging-wireless-50":63499,"battery-charging-wireless-60":63500,"battery-charging-wireless-70":63501,"battery-charging-wireless-80":63502,"battery-charging-wireless-90":63503,"battery-charging-wireless-alert":63504,"battery-heart":983610,"battery-heart-variant":983612,"battery-high":983758,"battery-low":983756,"battery-medium":983757,"battery-minus-variant":61580,"battery-negative":61581,"battery-off":983688,"battery-plus-variant":61583,"battery-positive":61584,"battery-unknown":61585,"battery-unknown-bluetooth":63817,"beach":61586,"beaker":64710,"beaker-alert":983636,"beaker-check":983638,"beaker-minus":983640,"beaker-plus":983642,"beaker-question":983644,"beaker-remove":983646,"bed":62179,"bed-double":983186,"bed-empty":63647,"bed-king":983188,"bed-queen":983190,"bed-single":983192,"bee":65473,"bee-flower":65474,"beer":61592,"bell":61594,"bell-alert":64821,"bell-badge":983446,"bell-cancel":988135,"bell-check":983568,"bell-circle":64822,"bell-minus":988137,"bell-off":61595,"bell-plus":61597,"bell-remove":988139,"bell-ring":61598,"bell-sleep":61600,"beta":61601,"betamax":63946,"biathlon":65015,"bicycle":983239,"bicycle-basket":983648,"bike":61603,"bike-fast":983370,"billboard":983090,"billiards":64317,"billiards-rack":64318,"binoculars":61605,"bio":61606,"biohazard":61607,"blender":64711,"blinds":61612,"blinds-open":983091,"block-helper":61613,"blood-bag":64712,"bluetooth-audio":61616,"bluetooth-connect":61617,"bluetooth-off":61618,"bluetooth-settings":61619,"bluetooth-transfer":61620,"blur":61621,"blur-linear":61622,"blur-off":61623,"blur-radial":61624,"bolt":64911,"bomb":63120,"bomb-off":63172,"bone":61625,"book":61626,"book-account":988077,"book-alphabet":63005,"book-cross":61602,"book-information-variant":983194,"book-lock":63385,"book-lock-open":63386,"book-minus":62937,"book-minus-multiple":64147,"book-multiple":61627,"book-music":61543,"book-open":61629,"book-open-blank-variant":61630,"book-open-page-variant":62938,"book-play":65183,"book-plus":62939,"book-plus-multiple":64148,"book-remove":64150,"book-remove-multiple":64149,"book-search":65185,"book-variant":61631,"bookmark":61632,"bookmark-check":61633,"bookmark-minus":63947,"bookmark-multiple":65016,"bookmark-music":61634,"bookmark-off":63949,"bookmark-plus":61637,"bookmark-remove":61638,"bookshelf":983690,"boom-gate":65187,"boom-gate-alert":65188,"boom-gate-arrow-down":65190,"boom-gate-arrow-up":65193,"boombox":62940,"boomerang":983290,"border-all":61639,"border-all-variant":63648,"border-bottom":61640,"border-bottom-variant":63649,"border-color":61641,"border-horizontal":61642,"border-inside":61643,"border-left":61644,"border-left-variant":63650,"border-none":61645,"border-none-variant":63651,"border-outside":61646,"border-right":61647,"border-right-variant":63652,"border-style":61648,"border-top":61649,"border-top-variant":63653,"border-vertical":61650,"bottle-soda":983195,"bottle-soda-classic":983196,"bottle-tonic":983385,"bottle-tonic-plus":983387,"bottle-tonic-skull":983389,"bottle-wine":63571,"bow-tie":63095,"bowl":8088,"bowl-mix":62999,"bowling":61651,"box-cutter":61653,"box-cutter-off":985930,"box-shadow":63031,"boxing-glove":64321,"braille":63951,"brain":63952,"bread-slice":64714,"bridge":63000,"briefcase":61654,"briefcase-account":64716,"briefcase-check":61655,"briefcase-clock":983291,"briefcase-download":61656,"briefcase-edit":64151,"briefcase-minus":64041,"briefcase-plus":64042,"briefcase-remove":64043,"briefcase-search":64044,"briefcase-upload":61657,"briefcase-variant":988308,"brightness-1":61658,"brightness-2":61659,"brightness-3":61660,"brightness-4":61661,"brightness-5":61662,"brightness-6":61663,"brightness-7":61664,"brightness-auto":61665,"brightness-percent":64718,"broom":61666,"brush":61667,"bucket":988181,"buffet":984440,"bug":61668,"bug-check":64045,"bugle":64912,"bulldozer":64263,"bullet":64719,"bulletin-board":61669,"bullhorn":61670,"bullseye":62941,"bullseye-arrow":63688,"bunk-bed":983853,"bus":61671,"bus-alert":64152,"bus-articulated-end":63387,"bus-articulated-front":63388,"bus-clock":63689,"bus-double-decker":63389,"bus-marker":983613,"bus-multiple":65372,"bus-school":63390,"bus-side":63391,"bus-stop":983092,"bus-stop-covered":983093,"bus-stop-uncovered":983094,"cable-data":988052,"cached":61672,"cactus":64913,"cake":61673,"cake-layered":61674,"cake-variant":61675,"calculator":61676,"calculator-variant":64153,"calendar":61677,"calendar-account":65268,"calendar-alert":64048,"calendar-arrow-left":983391,"calendar-arrow-right":983392,"calendar-blank":61678,"calendar-blank-multiple":983198,"calendar-check":61679,"calendar-clock":61680,"calendar-edit":63654,"calendar-export":64265,"calendar-heart":63953,"calendar-import":64266,"calendar-minus":64824,"calendar-month":65018,"calendar-multiple":61681,"calendar-multiple-check":61682,"calendar-multiselect":64049,"calendar-plus":61683,"calendar-question":63121,"calendar-range":63096,"calendar-refresh":20081,"calendar-remove":61684,"calendar-search":63819,"calendar-star":63954,"calendar-sync":65195,"calendar-text":61685,"calendar-today":61686,"calendar-week":64050,"calendar-week-begin":64051,"calendar-weekend":65270,"call-made":61687,"call-merge":61688,"call-missed":61689,"call-received":61690,"call-split":61691,"camcorder":61692,"camcorder-off":61695,"camera":61696,"camera-account":63690,"camera-burst":63122,"camera-control":64325,"camera-enhance":61697,"camera-front":61698,"camera-front-variant":61699,"camera-gopro":63392,"camera-image":63691,"camera-iris":61700,"camera-metering-center":63393,"camera-metering-matrix":63394,"camera-metering-partial":63395,"camera-metering-spot":63396,"camera-off":62943,"camera-party-mode":61701,"camera-plus":65272,"camera-rear":61702,"camera-rear-variant":61703,"camera-retake":65020,"camera-switch":61704,"camera-timer":61705,"camera-wireless":64914,"campfire":65274,"cancel":63289,"candle":62946,"candycane":61706,"cannabis":63397,"caps-lock":64154,"car":61707,"car-2-plus":983095,"car-3-plus":983096,"car-arrow-left":988082,"car-arrow-right":988083,"car-back":65022,"car-battery":61708,"car-brake-abs":64547,"car-brake-alert":64548,"car-brake-hold":64826,"car-brake-parking":64827,"car-brake-retarder":983097,"car-child-seat":65475,"car-clutch":983098,"car-cog":988108,"car-connected":61709,"car-convertible":63398,"car-coolant-level":983099,"car-cruise-control":64828,"car-defrost-front":64829,"car-defrost-rear":64830,"car-door":64327,"car-door-lock":983240,"car-electric":64328,"car-esp":64549,"car-estate":63399,"car-hatchback":63400,"car-info":983529,"car-key":64329,"car-light-dimmed":64550,"car-light-fog":64551,"car-light-high":64552,"car-limousine":63692,"car-multiple":64330,"car-off":65023,"car-parking-lights":64831,"car-pickup":63401,"car-seat":65476,"car-seat-cooler":65477,"car-seat-heater":65478,"car-settings":988109,"car-shift-pattern":65373,"car-side":63402,"car-sports":63403,"car-tire-alert":64553,"car-traction-control":64832,"car-turbocharger":983100,"car-wash":61710,"car-windshield":983101,"carabiner":988352,"caravan":63404,"card":64331,"card-account-details":62930,"card-account-details-star":983715,"card-account-mail":61838,"card-account-phone":65206,"card-bulleted":64332,"card-bulleted-off":64333,"card-bulleted-settings":64336,"card-plus":983594,"card-search":983199,"card-text":64339,"cards":63032,"cards-club":63693,"cards-diamond":63694,"cards-heart":63695,"cards-spade":63696,"cards-variant":63174,"carrot":61711,"cart":61712,"cart-arrow-down":64834,"cart-arrow-right":64554,"cart-arrow-up":64835,"cart-minus":64836,"cart-off":63083,"cart-plus":61714,"cart-remove":64837,"case-sensitive-alt":61715,"cash":61716,"cash-100":61717,"cash-marker":64916,"cash-minus":983691,"cash-multiple":61718,"cash-plus":983692,"cash-refund":64155,"cash-register":64720,"cash-remove":983693,"cassette":63955,"cast":61720,"cast-audio":983104,"cast-connected":61721,"cast-education":65133,"cast-off":63369,"castle":61722,"cat":61723,"cctv":63405,"ceiling-light":63336,"cellphone":61724,"cellphone-arrow-down":63956,"cellphone-basic":61726,"cellphone-charging":988055,"cellphone-cog":63824,"cellphone-dock":61727,"cellphone-information":65374,"cellphone-key":63821,"cellphone-link":61729,"cellphone-link-off":61730,"cellphone-lock":63822,"cellphone-message":63698,"cellphone-message-off":983293,"cellphone-nfc":65197,"cellphone-nfc-off":983811,"cellphone-off":63823,"cellphone-play":983105,"cellphone-remove":63820,"cellphone-screenshot":64052,"cellphone-settings":61731,"cellphone-sound":63825,"cellphone-text":63697,"cellphone-wireless":63508,"certificate":61732,"chair-rolling":65466,"chair-school":61733,"charity":64555,"chart-arc":61734,"chart-areaspline":61735,"chart-areaspline-variant":65198,"chart-bar":61736,"chart-bar-stacked":63337,"chart-bell-curve":64556,"chart-bell-curve-cumulative":65479,"chart-bubble":62947,"chart-donut":63406,"chart-donut-variant":63407,"chart-gantt":63084,"chart-histogram":61737,"chart-line":61738,"chart-line-stacked":63338,"chart-line-variant":63408,"chart-multiline":63699,"chart-multiple":983614,"chart-pie":61739,"chart-ppf":988032,"chart-sankey":983562,"chart-sankey-variant":983563,"chart-scatter-plot":65199,"chart-scatter-plot-hexbin":63085,"chart-timeline":63086,"chart-timeline-variant":65200,"chart-tree":65201,"chat":64341,"chat-alert":64342,"chat-minus":988176,"chat-plus":988175,"chat-processing":64343,"chat-remove":988177,"chat-sleep":983804,"check":61740,"check-all":61741,"check-bold":65134,"check-circle":62944,"check-decagram":63376,"check-network":64559,"check-underline":65136,"check-underline-circle":65137,"checkbook":64156,"checkbox-blank":61742,"checkbox-blank-badge":983457,"checkbox-blank-circle":61743,"checkbox-blank-off":983831,"checkbox-intermediate":63573,"checkbox-marked":61746,"checkbox-marked-circle":61747,"checkbox-multiple-blank":61750,"checkbox-multiple-blank-circle":63035,"checkbox-multiple-marked":61752,"checkbox-multiple-marked-circle":63037,"checkerboard":61754,"checkerboard-minus":983597,"checkerboard-plus":983596,"checkerboard-remove":983598,"cheese":983780,"cheese-off":988142,"chef-hat":64344,"chemical-weapon":61755,"chess-bishop":63579,"chess-king":63574,"chess-knight":63575,"chess-pawn":63576,"chess-queen":63577,"chess-rook":63578,"chevron-double-down":61756,"chevron-double-left":61757,"chevron-double-right":61758,"chevron-double-up":61759,"chevron-down":61760,"chevron-down-box":63957,"chevron-down-circle":64267,"chevron-left":61761,"chevron-left-box":63959,"chevron-left-circle":64269,"chevron-right":61762,"chevron-right-box":63961,"chevron-right-circle":64271,"chevron-triple-down":64917,"chevron-triple-left":64918,"chevron-triple-right":64919,"chevron-triple-up":64920,"chevron-up":61763,"chevron-up-box":63963,"chevron-up-circle":64273,"chili-hot":63409,"chili-medium":63410,"chili-mild":63411,"chili-off":988263,"chip":63002,"church":61764,"cigar":983476,"cigar-off":988187,"circle":63332,"circle-double":65202,"circle-expand":65203,"circle-half":988053,"circle-half-full":988054,"circle-medium":63965,"circle-multiple":985912,"circle-slice-1":64157,"circle-slice-2":64158,"circle-slice-3":64159,"circle-slice-4":64160,"circle-slice-5":64161,"circle-slice-6":64162,"circle-slice-7":64163,"circle-slice-8":64164,"circle-small":63966,"circular-saw":65139,"city":61766,"city-variant":64053,"clipboard":61767,"clipboard-account":61768,"clipboard-alert":61769,"clipboard-arrow-down":61770,"clipboard-arrow-left":61771,"clipboard-arrow-right":64725,"clipboard-arrow-up":64563,"clipboard-check":61772,"clipboard-check-multiple":983694,"clipboard-file":983696,"clipboard-flow":63175,"clipboard-list":983295,"clipboard-multiple":983698,"clipboard-play":64565,"clipboard-play-multiple":983700,"clipboard-plus":63312,"clipboard-pulse":63580,"clipboard-text":61774,"clipboard-text-multiple":983702,"clipboard-text-play":64567,"clock":63827,"clock-alert":63828,"clock-check":65480,"clock-digital":65204,"clock-end":61777,"clock-fast":61778,"clock-in":61779,"clock-out":61780,"clock-start":61781,"clock-time-eight":988230,"clock-time-eleven":988233,"clock-time-five":988227,"clock-time-four":988226,"clock-time-nine":988231,"clock-time-one":988223,"clock-time-seven":988229,"clock-time-six":988228,"clock-time-ten":988232,"clock-time-three":988225,"clock-time-twelve":988234,"clock-time-two":988224,"close":61782,"close-box":61783,"close-box-multiple":64569,"close-circle":61785,"close-circle-multiple":984618,"close-network":61787,"close-octagon":61788,"close-thick":988056,"closed-caption":61790,"cloud":61791,"cloud-alert":63967,"cloud-braces":63412,"cloud-check":61792,"cloud-circle":61793,"cloud-download":61794,"cloud-lock":983580,"cloud-print":61797,"cloud-question":64056,"cloud-refresh":984362,"cloud-search":63829,"cloud-sync":63039,"cloud-tags":63413,"cloud-upload":61799,"clover":63509,"coach-lamp":983106,"coat-rack":983241,"code-array":61800,"code-braces":61801,"code-braces-box":983297,"code-brackets":61802,"code-equal":61803,"code-greater-than":61804,"code-greater-than-or-equal":61805,"code-json":63014,"code-less-than":61806,"code-less-than-or-equal":61807,"code-not-equal":61808,"code-not-equal-variant":61809,"code-parentheses":61810,"code-parentheses-box":983298,"code-string":61811,"code-tags":61812,"code-tags-check":63123,"coffee":61814,"coffee-maker":983242,"coffee-off":65482,"coffee-to-go":61815,"coffin":64347,"cog":62611,"cog-box":62612,"cog-clockwise":983560,"cog-counterclockwise":983561,"cog-off":988110,"cog-refresh":988254,"cog-sync":988256,"cog-transfer":983165,"cogs":63701,"collage":63040,"collapse-all":64165,"color-helper":61817,"comma":65140,"comma-box":65141,"comma-circle":65143,"comment":61818,"comment-account":61819,"comment-alert":61821,"comment-arrow-left":63968,"comment-arrow-right":63970,"comment-check":61823,"comment-edit":983530,"comment-eye":64057,"comment-multiple":63582,"comment-plus":63972,"comment-processing":61828,"comment-question":63510,"comment-quote":983107,"comment-remove":62942,"comment-search":64059,"comment-text":61832,"comment-text-multiple":63583,"compare":61834,"compare-horizontal":988306,"compare-vertical":988307,"compass":61835,"compass-off":64348,"compass-rose":988034,"console":61837,"console-line":63414,"console-network":63656,"consolidate":983299,"contactless-payment":64838,"contactless-payment-circle":3362,"contacts":63178,"contain":64061,"contain-end":64062,"contain-start":64063,"content-copy":61839,"content-cut":61840,"content-duplicate":61841,"content-paste":61842,"content-save":61843,"content-save-alert":65375,"content-save-all":61844,"content-save-cog":988251,"content-save-edit":64727,"content-save-move":65145,"content-save-settings":63003,"contrast":61845,"contrast-box":61846,"contrast-circle":61847,"controller":62132,"controller-classic":64350,"controller-off":62133,"cookie":61848,"coolant-temperature":62408,"copyright":62950,"corn":63415,"corn-off":988143,"cosine-wave":988281,"counter":61849,"cow":61850,"cpu-32-bit":65276,"cpu-64-bit":65277,"crane":63585,"creation":61897,"credit-card":983056,"credit-card-check":988112,"credit-card-clock":65278,"credit-card-marker":63143,"credit-card-minus":65484,"credit-card-multiple":983057,"credit-card-off":983058,"credit-card-plus":983059,"credit-card-refund":983060,"credit-card-remove":65486,"credit-card-scan":983061,"credit-card-settings":983062,"credit-card-wireless":63489,"credit-card-wireless-off":984442,"cricket":64841,"crop":61854,"crop-free":61855,"crop-landscape":61856,"crop-portrait":61857,"crop-rotate":63125,"crop-square":61858,"cross":63826,"cross-bolnisi":64713,"cross-celtic":64721,"crosshairs":61859,"crosshairs-gps":61860,"crosshairs-off":65378,"crosshairs-question":983393,"crown":61861,"crystal-ball":64276,"cube":61862,"cube-off":988188,"cube-scan":64352,"cube-send":61864,"cube-unfolded":61865,"cup":61866,"cup-off":62949,"cup-water":61867,"cupboard":65379,"cupcake":63833,"curling":63586,"currency-bdt":63587,"currency-brl":64353,"currency-btc":61868,"currency-cny":63417,"currency-eth":63418,"currency-eur":61869,"currency-eur-off":983872,"currency-gbp":61870,"currency-ils":64573,"currency-inr":61871,"currency-jpy":63419,"currency-krw":63420,"currency-kzt":63588,"currency-ngn":61872,"currency-php":63973,"currency-rial":65209,"currency-rub":61873,"currency-sign":63421,"currency-try":61874,"currency-twd":63422,"currency-usd":61875,"currency-usd-off":63097,"current-ac":988288,"current-dc":63835,"cursor-default":61876,"cursor-default-click":64729,"cursor-default-gesture":983378,"cursor-move":61878,"cursor-pointer":61879,"cursor-text":62951,"database":61880,"database-check":64168,"database-edit":64354,"database-export":63837,"database-import":63836,"database-lock":64169,"database-marker":983841,"database-minus":61881,"database-plus":61882,"database-refresh":984514,"database-remove":64732,"database-search":63589,"database-settings":64733,"database-sync":64731,"death-star":63703,"death-star-variant":63704,"deathly-hallows":64355,"debug-step-into":61883,"debug-step-out":61884,"debug-step-over":61885,"decagram":63339,"decimal":983244,"decimal-comma":983245,"decimal-comma-decrease":983246,"decimal-comma-increase":983247,"decimal-decrease":61886,"decimal-increase":61887,"delete":61888,"delete-alert":983248,"delete-circle":63106,"delete-empty":63179,"delete-forever":62952,"delete-off":983250,"delete-restore":63512,"delete-sweep":62953,"delete-variant":61889,"delta":61890,"desk":983652,"desk-lamp":63838,"deskphone":61891,"desktop-classic":63423,"desktop-tower":61893,"desktop-tower-monitor":64170,"details":61894,"developer-board":63126,"devices":65488,"dharmachakra":63818,"diabetes":983377,"dialpad":63004,"diameter":64575,"diameter-variant":64577,"diamond":64358,"diamond-stone":61896,"dice-1":61898,"dice-2":61899,"dice-3":61900,"dice-4":61901,"dice-5":61902,"dice-6":61903,"dice-d10":983422,"dice-d12":983423,"dice-d20":983424,"dice-d4":983419,"dice-d6":983420,"dice-d8":983421,"dice-multiple":63341,"dip-switch":63424,"directions":61904,"directions-fork":63041,"disc":62958,"disc-alert":61905,"disc-player":63839,"dishwasher":64171,"dishwasher-alert":983523,"dishwasher-off":983524,"distribute-horizontal-center":983540,"distribute-horizontal-left":983539,"distribute-horizontal-right":983541,"distribute-vertical-bottom":983542,"distribute-vertical-center":983543,"distribute-vertical-top":983544,"diving-flippers":64923,"diving-helmet":64924,"diving-scuba-flag":64926,"diving-scuba-mask":64925,"diving-scuba-tank":64927,"diving-scuba-tank-multiple":64928,"diving-snorkel":64929,"division":61908,"division-box":61909,"dna":63107,"dns":61910,"dock-bottom":983252,"dock-left":983253,"dock-right":983254,"dock-window":983255,"doctor":64065,"dog":64066,"dog-service":64172,"dog-side":64067,"dolly":65211,"domain":61911,"domain-off":64843,"domain-plus":983256,"domain-remove":983257,"dome-light":988190,"domino-mask":983109,"donkey":63425,"door":63513,"door-closed":63514,"door-closed-lock":983258,"door-open":63515,"doorbell":983825,"doorbell-video":63592,"dots-horizontal":61912,"dots-horizontal-circle":63426,"dots-vertical":61913,"dots-vertical-circle":63427,"download":61914,"download-box":988258,"download-circle":988260,"download-lock":987936,"download-multiple":63976,"download-network":63219,"download-off":983259,"drag":61915,"drag-horizontal":61916,"drag-horizontal-variant":983835,"drag-variant":64364,"drag-vertical":61917,"drag-vertical-variant":983836,"drama-masks":64734,"draw":65382,"drawing":61918,"drawing-box":61919,"dresser":65383,"drone":61922,"duck":61925,"dumbbell":61926,"dump-truck":64579,"ear-hearing":63428,"ear-hearing-off":64068,"earth":61927,"earth-arrow-right":983868,"earth-box":63180,"earth-box-minus":988167,"earth-box-off":63181,"earth-box-plus":988166,"earth-box-remove":988168,"earth-minus":988164,"earth-off":61928,"earth-plus":988163,"earth-remove":988165,"egg":64174,"egg-easter":64175,"egg-off":988144,"eight-track":63977,"eject":61930,"electric-switch":65212,"electric-switch-closed":983300,"elephant":63429,"elevation-decline":61931,"elevation-rise":61932,"elevator":61933,"elevator-down":983789,"elevator-passenger":988033,"elevator-up":983788,"ellipse":65213,"email":61934,"email-alert":63182,"email-arrow-left":983301,"email-arrow-right":983303,"email-box":64735,"email-check":64176,"email-edit":65280,"email-lock":61937,"email-mark-as-unread":64366,"email-minus":65282,"email-multiple":65284,"email-newsletter":65489,"email-off":988131,"email-open":61935,"email-open-multiple":65286,"email-plus":63978,"email-search":63840,"email-sync":983794,"email-variant":62960,"emoticon":64580,"emoticon-angry":64581,"emoticon-confused":983305,"emoticon-cool":64583,"emoticon-cry":64584,"emoticon-dead":64586,"emoticon-devil":64587,"emoticon-excited":64588,"emoticon-frown":65385,"emoticon-happy":64589,"emoticon-kiss":64590,"emoticon-lol":983615,"emoticon-neutral":64592,"emoticon-poop":61943,"emoticon-sad":64594,"emoticon-tongue":61945,"emoticon-wink":64596,"engine":61946,"engine-off":64069,"epsilon":983307,"equal":61948,"equal-box":61949,"equalizer":65215,"eraser":61950,"eraser-variant":63042,"escalator":61951,"escalator-box":988057,"escalator-down":983787,"escalator-up":983786,"et":64178,"ethernet":61952,"ethernet-cable":61953,"ethernet-cable-off":61954,"ev-station":62961,"excavator":983111,"exclamation":61957,"exclamation-thick":983651,"exit-run":64071,"exit-to-app":61958,"expand-all":64179,"expansion-card":63661,"expansion-card-variant":65490,"exponent":63842,"exponent-box":63843,"export":61959,"export-variant":64367,"eye":61960,"eye-check":64736,"eye-circle":64368,"eye-minus":983112,"eye-off":61961,"eye-plus":63594,"eye-settings":63596,"eyedropper":61962,"eyedropper-minus":988125,"eyedropper-off":988127,"eyedropper-plus":988124,"eyedropper-remove":988126,"eyedropper-variant":61963,"face-agent":64844,"face-man":63043,"face-man-profile":63044,"face-recognition":64599,"face-woman":983202,"face-woman-profile":983201,"factory":61967,"fan":61968,"fan-alert":988268,"fan-chevron-down":988269,"fan-chevron-up":988270,"fan-minus":988272,"fan-off":63516,"fan-plus":988271,"fan-remove":988273,"fan-speed-1":988274,"fan-speed-2":988275,"fan-speed-3":988276,"fast-forward":61969,"fast-forward-10":64845,"fast-forward-30":64738,"fast-forward-5":983587,"fax":61970,"feather":63186,"feature-search":64072,"fencing":988353,"ferris-wheel":65217,"ferry":61971,"file":61972,"file-account":63290,"file-alert":64074,"file-cabinet":64181,"file-cad":65288,"file-cad-box":65289,"file-cancel":64930,"file-certificate":983473,"file-chart":61973,"file-check":61974,"file-clock":983820,"file-cloud":61975,"file-code":61998,"file-cog":983206,"file-compare":63657,"file-delimited":61976,"file-document":61977,"file-document-edit":64932,"file-download":63844,"file-edit":983570,"file-excel":61979,"file-excel-box":61980,"file-export":61981,"file-eye":64934,"file-find":61982,"file-gif-box":64852,"file-hidden":62995,"file-image":61983,"file-import":61984,"file-jpg-box":61989,"file-key":983471,"file-link":983458,"file-lock":61985,"file-move":64184,"file-multiple":61986,"file-music":61987,"file-pdf-box":61990,"file-percent":63517,"file-phone":983460,"file-plus":63313,"file-powerpoint":61991,"file-powerpoint-box":61992,"file-presentation-box":61993,"file-question":63598,"file-refresh":985368,"file-remove":64372,"file-replace":64279,"file-restore":63088,"file-search":64600,"file-send":61994,"file-settings":983204,"file-star":983132,"file-swap":65492,"file-sync":983617,"file-table":64602,"file-table-box":983308,"file-table-box-multiple":983309,"file-tree":63045,"file-undo":63707,"file-upload":64076,"file-video":61995,"file-word":61996,"file-word-box":61997,"film":61999,"filmstrip":62000,"filmstrip-box":38868,"filmstrip-box-multiple":64756,"filmstrip-off":62001,"filter":62002,"filter-menu":983312,"filter-minus":65291,"filter-plus":65293,"filter-remove":62004,"filter-variant":62006,"filter-variant-minus":983357,"filter-variant-plus":983358,"filter-variant-remove":983137,"finance":63518,"find-replace":63187,"fingerprint":62007,"fingerprint-off":65230,"fire":62008,"fire-extinguisher":65295,"fire-hydrant":983394,"fire-hydrant-alert":983395,"fire-hydrant-off":983396,"fire-truck":63658,"fireplace":65041,"fireplace-off":65042,"firework":65043,"fish":62010,"fish-off":988147,"fishbowl":65296,"fit-to-page":65298,"flag":62011,"flag-checkered":62012,"flag-minus":64373,"flag-plus":64374,"flag-remove":64375,"flag-triangle":62015,"flag-variant":62016,"flare":64846,"flash":62017,"flash-alert":65300,"flash-auto":62018,"flash-off":62019,"flash-red-eye":63098,"flashlight":62020,"flashlight-off":62021,"flask":61587,"flask-empty":61588,"flask-empty-minus":983653,"flask-empty-off":988148,"flask-empty-plus":983655,"flask-empty-remove":983657,"flask-minus":983659,"flask-off":988150,"flask-plus":983661,"flask-remove":983663,"flask-round-bottom":983670,"flask-round-bottom-empty":983671,"fleur-de-lis":983854,"flip-horizontal":983314,"flip-to-back":62023,"flip-to-front":62024,"flip-vertical":983315,"floor-lamp":63708,"floor-lamp-dual":983138,"floor-lamp-torchiere-variant":983139,"floor-plan":63520,"floppy":62025,"floppy-variant":63982,"flower":62026,"flower-poppy":64740,"flower-tulip":63984,"focus-auto":65387,"focus-field":65388,"focus-field-horizontal":65389,"focus-field-vertical":65390,"folder":62027,"folder-account":62028,"folder-alert":64936,"folder-clock":64185,"folder-cog":983210,"folder-download":62029,"folder-edit":63709,"folder-google-drive":62030,"folder-heart":983317,"folder-home":983264,"folder-image":62031,"folder-information":983266,"folder-key":63659,"folder-key-network":63660,"folder-lock":62032,"folder-lock-open":62033,"folder-marker":983704,"folder-move":62034,"folder-multiple":62035,"folder-multiple-image":62036,"folder-multiple-plus":988286,"folder-music":987993,"folder-network":63599,"folder-open":63343,"folder-plus":62039,"folder-pound":64741,"folder-refresh":984905,"folder-remove":62040,"folder-search":63847,"folder-settings":983208,"folder-star":63132,"folder-star-multiple":988115,"folder-swap":65494,"folder-sync":64743,"folder-table":983822,"folder-text":64606,"folder-upload":62041,"folder-zip":63210,"food":62042,"food-apple":62043,"food-croissant":63431,"food-drumstick":988191,"food-drumstick-off":988264,"food-fork-drink":62962,"food-off":62963,"food-steak":988266,"food-steak-off":988267,"food-variant":62044,"food-variant-off":988133,"foot-print":65391,"football":62045,"football-australian":62046,"football-helmet":62047,"forklift":63432,"form-dropdown":988160,"form-select":988161,"form-textarea":983232,"form-textbox":62990,"form-textbox-lock":987997,"form-textbox-password":63476,"format-align-bottom":63314,"format-align-center":62048,"format-align-justify":62049,"format-align-left":62050,"format-align-middle":63315,"format-align-right":62051,"format-align-top":63316,"format-annotation-minus":64187,"format-annotation-plus":63046,"format-bold":62052,"format-clear":62053,"format-color-fill":62054,"format-color-highlight":65044,"format-color-marker-cancel":983870,"format-color-text":63133,"format-columns":63710,"format-float-center":62055,"format-float-left":62056,"format-float-none":62057,"format-float-right":62058,"format-font":63189,"format-font-size-decrease":63986,"format-font-size-increase":63987,"format-header-1":62059,"format-header-2":62060,"format-header-3":62061,"format-header-4":62062,"format-header-5":62063,"format-header-6":62064,"format-header-decrease":62065,"format-header-equal":62066,"format-header-increase":62067,"format-header-pound":62068,"format-horizontal-align-center":63006,"format-horizontal-align-left":63007,"format-horizontal-align-right":63008,"format-indent-decrease":62069,"format-indent-increase":62070,"format-italic":62071,"format-letter-case":64281,"format-letter-case-lower":64282,"format-letter-case-upper":64283,"format-letter-ends-with":65496,"format-letter-matches":65497,"format-letter-starts-with":65498,"format-line-spacing":62072,"format-line-style":62920,"format-line-weight":62921,"format-list-bulleted":62073,"format-list-bulleted-square":64940,"format-list-bulleted-triangle":65231,"format-list-bulleted-type":62074,"format-list-checkbox":63849,"format-list-checks":63317,"format-list-numbered":62075,"format-list-numbered-rtl":64745,"format-list-text":983706,"format-overline":65232,"format-page-break":63190,"format-paint":62076,"format-paragraph":62077,"format-pilcrow":63191,"format-pilcrow-arrow-left":62086,"format-pilcrow-arrow-right":62085,"format-quote-close":62078,"format-quote-open":63318,"format-rotate-90":63145,"format-section":63134,"format-size":62079,"format-strikethrough":62080,"format-strikethrough-variant":62081,"format-subscript":62082,"format-superscript":62083,"format-text":62084,"format-text-rotation-angle-down":65499,"format-text-rotation-angle-up":65500,"format-text-rotation-down":64847,"format-text-rotation-down-vertical":65501,"format-text-rotation-none":64848,"format-text-rotation-up":65502,"format-text-rotation-vertical":65503,"format-text-variant":65045,"format-text-wrapping-clip":64746,"format-text-wrapping-overflow":64747,"format-text-wrapping-wrap":64748,"format-textbox":64749,"format-title":62964,"format-underline":62087,"format-vertical-align-bottom":63009,"format-vertical-align-center":63010,"format-vertical-align-top":63011,"format-wrap-inline":62088,"format-wrap-square":62089,"format-wrap-tight":62090,"format-wrap-top-bottom":62091,"forum":62092,"forward":62093,"forwardburger":64849,"fountain":63850,"fountain-pen":64750,"fountain-pen-tip":64751,"frequently-asked-questions":65233,"fridge":62096,"fridge-alert":983516,"fridge-bottom":62098,"fridge-off":983514,"fridge-top":62097,"fruit-cherries":983140,"fruit-cherries-off":988152,"fruit-citrus":983141,"fruit-citrus-off":988153,"fruit-grapes":983142,"fruit-pineapple":983144,"fruit-watermelon":983145,"fuel":63433,"fullscreen":62099,"fullscreen-exit":62100,"function":62101,"function-variant":63600,"furigana-horizontal":983212,"furigana-vertical":983213,"fuse":64609,"fuse-alert":988205,"fuse-blade":64610,"fuse-off":988204,"gamepad":62102,"gamepad-circle":65046,"gamepad-circle-down":65047,"gamepad-circle-left":65048,"gamepad-circle-right":65050,"gamepad-circle-up":65051,"gamepad-down":65052,"gamepad-left":65053,"gamepad-right":65054,"gamepad-round":65055,"gamepad-round-down":65150,"gamepad-round-left":65151,"gamepad-round-right":65153,"gamepad-round-up":65154,"gamepad-square":65234,"gamepad-up":65155,"gamepad-variant":62103,"gamma":983321,"gantry-crane":64941,"garage":63192,"garage-alert":63601,"garage-alert-variant":983808,"garage-open":63193,"garage-open-variant":983807,"garage-variant":983806,"gas-cylinder":63047,"gas-station":62104,"gas-station-off":988169,"gate":62105,"gate-and":63712,"gate-arrow-right":983444,"gate-nand":63713,"gate-nor":63714,"gate-not":63715,"gate-open":983445,"gate-or":63716,"gate-xnor":63717,"gate-xor":63718,"gauge":62106,"gauge-empty":63602,"gauge-full":63603,"gauge-low":63604,"gavel":62107,"gender-female":62108,"gender-male":62109,"gender-male-female":62110,"gender-male-female-variant":983402,"gender-non-binary":983403,"gender-transgender":62111,"gesture":63434,"gesture-double-tap":63291,"gesture-pinch":64188,"gesture-spread":64189,"gesture-swipe":64850,"gesture-swipe-down":63292,"gesture-swipe-horizontal":64190,"gesture-swipe-left":63293,"gesture-swipe-right":63294,"gesture-swipe-up":63295,"gesture-swipe-vertical":64191,"gesture-tap":63296,"gesture-tap-box":983764,"gesture-tap-button":983763,"gesture-tap-hold":64851,"gesture-two-double-tap":63297,"gesture-two-tap":63298,"ghost":62112,"ghost-off":63988,"gift":65157,"glass-cocktail":62294,"glass-flute":62117,"glass-mug":62118,"glass-mug-variant":983361,"glass-stange":62119,"glass-tulip":62120,"glass-wine":63605,"glasses":62122,"globe-light-outline":983810,"globe-model":63720,"go-kart":64853,"go-kart-track":64854,"gold":983674,"golf":63522,"golf-cart":983503,"golf-tee":983214,"gondola":63109,"google-circles":62128,"google-circles-communities":62129,"google-circles-extended":62130,"google-circles-group":62131,"google-downasaur":988002,"gradient-vertical":63135,"grain":64856,"graph":983147,"grave-stone":64382,"grease-pencil":63048,"greater-than":63852,"greater-than-or-equal":63853,"grid":62145,"grid-large":63319,"grid-off":62146,"grill":65158,"group":62147,"guitar-acoustic":63344,"guitar-electric":62148,"guitar-pick":62149,"guy-fawkes-mask":63524,"hail":64192,"hair-dryer":983322,"halloween":64383,"hamburger":63108,"hammer":63721,"hammer-screwdriver":987938,"hammer-wrench":987939,"hand-back-left":65159,"hand-back-right":65160,"hand-front-right":64078,"hand-heart":983324,"hand-okay":64079,"hand-peace":64080,"hand-peace-variant":64081,"hand-pointing-down":64082,"hand-pointing-left":64083,"hand-pointing-right":62151,"hand-pointing-up":64084,"hand-saw":65161,"hand-water":988063,"handball":65392,"handcuffs":983401,"handshake":983619,"hanger":62152,"hard-hat":63854,"harddisk":62154,"harddisk-plus":983149,"harddisk-remove":983150,"hat-fedora":64384,"hazard-lights":64613,"hdr":64857,"hdr-off":64858,"head":987998,"head-alert":987960,"head-check":987962,"head-cog":987964,"head-dots-horizontal":987966,"head-flash":987968,"head-heart":987970,"head-lightbulb":987972,"head-minus":987974,"head-plus":987976,"head-question":987978,"head-remove":987980,"head-snowflake":987982,"head-sync":987984,"headphones":62155,"headphones-bluetooth":63855,"headphones-box":62156,"headphones-off":63437,"headphones-settings":62157,"headset":62158,"headset-dock":62159,"headset-off":62160,"heart":62161,"heart-box":62162,"heart-broken":62164,"heart-circle":63856,"heart-flash":65302,"heart-half":63198,"heart-half-full":63197,"heart-minus":988207,"heart-multiple":64085,"heart-off":63320,"heart-plus":988206,"heart-pulse":62966,"heart-remove":988208,"helicopter":64193,"help":62166,"help-box":63370,"help-circle":62167,"help-network":63220,"help-rhombus":64385,"hexadecimal":983762,"hexagon":62168,"hexagon-multiple":63200,"hexagon-slice-1":64194,"hexagon-slice-2":64195,"hexagon-slice-3":64196,"hexagon-slice-4":64197,"hexagon-slice-5":64198,"hexagon-slice-6":64199,"hexagram":64200,"high-definition":63438,"high-definition-box":63607,"highway":62967,"hiking":64859,"history":62170,"hockey-puck":63608,"hockey-sticks":63609,"hololens":62171,"home":62172,"home-account":63525,"home-alert":63610,"home-analytics":65239,"home-automation":63440,"home-circle":63441,"home-city":64753,"home-edit":983428,"home-flood":65303,"home-floor-0":64942,"home-floor-1":64860,"home-floor-2":64861,"home-floor-3":64862,"home-floor-a":64863,"home-floor-b":64864,"home-floor-g":64865,"home-floor-l":64866,"home-floor-negative-1":64943,"home-group":64944,"home-heart":63526,"home-lightbulb":983676,"home-lock":63722,"home-lock-open":63723,"home-map-marker":62968,"home-minus":63859,"home-modern":62173,"home-plus":63860,"home-remove":983666,"home-roof":983382,"home-search":988080,"home-thermometer":65393,"home-variant":62174,"hook":63201,"hook-off":63202,"hoop-house":65081,"hops":62175,"horizontal-rotate-clockwise":983326,"horizontal-rotate-counterclockwise":983327,"horseshoe":64087,"hospital":983063,"hospital-box":62176,"hospital-building":62177,"hospital-marker":62178,"hot-tub":63527,"hours-24":988280,"human":62182,"human-baby-changing-table":988043,"human-child":62183,"human-female":63049,"human-female-boy":64088,"human-female-female":64089,"human-female-girl":64090,"human-greeting-variant":63050,"human-handsdown":63051,"human-handsup":63052,"human-male":63053,"human-male-board":63631,"human-male-boy":64091,"human-male-child":988044,"human-male-female":62184,"human-male-girl":64092,"human-male-height":65304,"human-male-height-variant":65305,"human-male-male":64093,"human-pregnant":62927,"human-scooter":983572,"human-wheelchair":988045,"hvac":987986,"hydraulic-oil-level":987940,"hydraulic-oil-temperature":987941,"hydro-power":983824,"ice-cream":63529,"ice-cream-off":986706,"ice-pop":65306,"id-card":65504,"identifier":65307,"ideogram-cjk":987953,"ideogram-cjk-variant":987954,"image":62185,"image-album":62186,"image-area":62187,"image-area-close":62188,"image-auto-adjust":65505,"image-broken":62189,"image-broken-variant":62190,"image-edit":983566,"image-filter-black-white":62192,"image-filter-center-focus":62193,"image-filter-center-focus-strong":65308,"image-filter-center-focus-weak":62194,"image-filter-drama":62195,"image-filter-frames":62196,"image-filter-hdr":62197,"image-filter-none":62198,"image-filter-tilt-shift":62199,"image-filter-vintage":62200,"image-frame":65162,"image-minus":988185,"image-move":63991,"image-multiple":62201,"image-off":63530,"image-plus":63611,"image-remove":988184,"image-search":63862,"image-size-select-actual":64617,"image-size-select-large":64618,"image-size-select-small":64619,"import":62202,"inbox":63110,"inbox-arrow-down":62203,"inbox-arrow-up":62417,"inbox-full":983709,"inbox-multiple":63663,"incognito":62969,"incognito-circle":988193,"incognito-circle-off":988194,"incognito-off":1713,"infinity":63203,"information":62204,"information-variant":63054,"instrument-triangle":983152,"invert-colors":62209,"invert-colors-off":65163,"ip":64094,"ip-network":64095,"ipod":64621,"island":983153,"iv-bag":983268,"jeepney":62210,"jellyfish":65310,"jump-rope":983850,"kabaddi":64867,"karate":63531,"kayaking":63662,"keg":62213,"kettle":62970,"kettle-alert":983874,"kettle-off":983878,"kettle-steam":983876,"kettlebell":983851,"key":62214,"key-arrow-right":983869,"key-change":62215,"key-link":983498,"key-minus":62216,"key-plus":62217,"key-remove":62218,"key-star":983497,"key-variant":62219,"key-wireless":65506,"keyboard":62220,"keyboard-backspace":62221,"keyboard-caps":62222,"keyboard-close":62223,"keyboard-esc":983778,"keyboard-f1":983766,"keyboard-f10":983775,"keyboard-f11":983776,"keyboard-f12":983777,"keyboard-f2":983767,"keyboard-f3":983768,"keyboard-f4":983769,"keyboard-f5":983770,"keyboard-f6":983771,"keyboard-f7":983772,"keyboard-f8":983773,"keyboard-f9":983774,"keyboard-off":62224,"keyboard-return":62225,"keyboard-settings":63992,"keyboard-space":983154,"keyboard-tab":62226,"keyboard-tab-reverse":62245,"keyboard-variant":62227,"khanda":983336,"klingon":987995,"knife":63994,"knife-military":63995,"label":62229,"label-multiple":988021,"label-off":64202,"label-percent":983829,"label-variant":64204,"ladybug":63532,"lambda":63015,"lamp":63156,"lan":62231,"lan-check":983765,"lan-connect":62232,"lan-disconnect":62233,"lan-pending":62234,"language-fortran":983621,"laptop":62242,"laptop-off":63206,"laser-pointer":988292,"lasso":65312,"latitude":65396,"launch":62247,"lava-lamp":63444,"layers":62248,"layers-minus":65165,"layers-off":62249,"layers-plus":65072,"layers-remove":65073,"layers-search":983601,"layers-triple":65397,"lead-pencil":63055,"leaf":62250,"leaf-maple":64623,"leaf-maple-off":983813,"leaf-off":983812,"leak":64947,"leak-off":64948,"led-off":62251,"led-on":62252,"led-strip":63445,"led-strip-variant":983155,"led-variant-off":62254,"led-variant-on":62255,"leek":983464,"less-than":63867,"less-than-or-equal":63868,"library":62257,"library-shelves":64389,"license":65507,"lifebuoy":63613,"light-switch":63869,"lightbulb":62261,"lightbulb-cfl":983603,"lightbulb-cfl-off":983604,"lightbulb-cfl-spiral":983712,"lightbulb-cfl-spiral-off":983790,"lightbulb-group":983678,"lightbulb-group-off":983800,"lightbulb-multiple":983680,"lightbulb-multiple-off":983802,"lightbulb-off":65074,"lightbulb-on":63207,"lighthouse":63998,"lighthouse-on":63999,"lightning-bolt":988171,"lightning-bolt-circle":63519,"lingerie":988278,"link":62263,"link-box":64758,"link-box-variant":64760,"link-lock":983269,"link-off":62264,"link-plus":64624,"link-variant":62265,"link-variant-minus":983338,"link-variant-off":62266,"link-variant-plus":983339,"link-variant-remove":983340,"lipstick":988085,"loading":63345,"location-enter":65508,"location-exit":65509,"lock":62270,"lock-alert":63725,"lock-check":988058,"lock-clock":63870,"lock-open":62271,"lock-open-alert":988059,"lock-open-check":988060,"lock-open-variant":65510,"lock-pattern":63209,"lock-plus":62971,"lock-question":63726,"lock-reset":63346,"lock-smart":63665,"locker":63446,"locker-multiple":63447,"login":62274,"login-variant":62972,"logout":62275,"logout-variant":62973,"longitude":65399,"looks":62276,"loupe":62277,"lungs":983215,"magazine-pistol":62244,"magazine-rifle":62243,"magnet":62279,"magnet-on":62280,"magnify":62281,"magnify-close":63871,"magnify-minus":62282,"magnify-minus-cursor":64097,"magnify-plus":62283,"magnify-plus-cursor":64098,"magnify-remove-cursor":983607,"magnify-scan":983713,"mail":65240,"mailbox":63213,"mailbox-open":64868,"mailbox-open-up":64870,"mailbox-up":64873,"map":62285,"map-check":65241,"map-clock":64762,"map-legend":64000,"map-marker":62286,"map-marker-alert":65314,"map-marker-check":64625,"map-marker-circle":62287,"map-marker-distance":63727,"map-marker-down":983341,"map-marker-left":983814,"map-marker-minus":63056,"map-marker-multiple":62288,"map-marker-off":62289,"map-marker-path":64764,"map-marker-plus":63057,"map-marker-question":65316,"map-marker-radius":62290,"map-marker-remove":65318,"map-marker-remove-variant":65319,"map-marker-right":983815,"map-marker-up":983342,"map-minus":63872,"map-plus":63874,"map-search":63875,"margin":62291,"marker":63058,"marker-cancel":64949,"marker-check":62293,"math-compass":62296,"math-cos":64626,"math-integral":65512,"math-integral-box":65513,"math-log":983216,"math-norm":65514,"math-norm-box":65515,"math-sin":64627,"math-tan":64628,"matrix":63016,"medal":63878,"medical-bag":63214,"meditation":983462,"memory":62299,"menu":62300,"menu-down":62301,"menu-left":62302,"menu-open":64391,"menu-right":62303,"menu-swap":64099,"menu-up":62304,"merge":65401,"message":62305,"message-alert":62306,"message-arrow-left":983837,"message-arrow-right":983839,"message-bulleted":63137,"message-bulleted-off":63138,"message-cog":63216,"message-draw":62307,"message-image":62308,"message-lock":65516,"message-minus":983449,"message-plus":63059,"message-processing":62310,"message-reply":62311,"message-reply-text":62312,"message-settings":63215,"message-text":62313,"message-text-clock":983454,"message-text-lock":65517,"message-video":62315,"metronome":63449,"metronome-tick":63450,"micro-sd":63451,"microphone":62316,"microphone-message":62730,"microphone-message-off":62731,"microphone-minus":63666,"microphone-off":62317,"microphone-plus":63667,"microphone-settings":62319,"microphone-variant":62320,"microphone-variant-off":62321,"microscope":63060,"microsoft-xbox-controller":62906,"microsoft-xbox-controller-battery-alert":63306,"microsoft-xbox-controller-battery-charging":64033,"microsoft-xbox-controller-battery-empty":63307,"microsoft-xbox-controller-battery-full":63308,"microsoft-xbox-controller-battery-low":63309,"microsoft-xbox-controller-battery-medium":63310,"microsoft-xbox-controller-battery-unknown":63311,"microsoft-xbox-controller-menu":65106,"microsoft-xbox-controller-off":62907,"microsoft-xbox-controller-view":65107,"microwave":64629,"microwave-off":988195,"middleware":65402,"midi-port":63729,"mine":64950,"mini-sd":64004,"minidisc":64005,"minus":62324,"minus-box":62325,"minus-box-multiple":983404,"minus-circle":62326,"minus-circle-multiple":983898,"minus-circle-off":988249,"minus-network":62328,"mirror":983592,"mixed-martial-arts":64875,"mixed-reality":63614,"molecule":64392,"molecule-co":983849,"molecule-co2":63459,"monitor":62329,"monitor-cellphone":63880,"monitor-cellphone-star":63881,"monitor-dashboard":64006,"monitor-edit":983793,"monitor-eye":988084,"monitor-lock":64951,"monitor-multiple":62330,"monitor-off":64876,"monitor-screenshot":65076,"monitor-share":988291,"monitor-shimmer":983343,"monitor-speaker":65404,"monitor-speaker-off":65405,"monitor-star":64952,"moon-first-quarter":65406,"moon-full":65407,"moon-last-quarter":65408,"moon-new":65409,"moon-waning-crescent":65410,"moon-waning-gibbous":65411,"moon-waxing-crescent":65412,"moon-waxing-gibbous":65413,"moped":983217,"more":62331,"mortar-pestle-plus":62449,"mother-heart":983871,"mother-nurse":64765,"motion-sensor":64877,"motion-sensor-off":988213,"motorbike":62332,"mouse":62333,"mouse-bluetooth":63882,"mouse-off":62334,"mouse-variant":62335,"mouse-variant-off":62336,"move-resize":63061,"move-resize-variant":63062,"movie":62337,"movie-edit":983373,"movie-filter":983375,"movie-open":65518,"movie-roll":63453,"movie-search":983549,"muffin":63883,"multiplication":62338,"multiplication-box":62339,"mushroom":63454,"mushroom-off":988154,"music":63321,"music-accidental-double-flat":65414,"music-accidental-double-sharp":65415,"music-accidental-flat":65416,"music-accidental-natural":65417,"music-accidental-sharp":65418,"music-box":62340,"music-box-multiple":62259,"music-circle":62342,"music-clef-alto":65419,"music-clef-bass":65420,"music-clef-treble":65421,"music-note":62343,"music-note-bluetooth":62974,"music-note-bluetooth-off":62975,"music-note-eighth":62344,"music-note-eighth-dotted":65422,"music-note-half":62345,"music-note-half-dotted":65423,"music-note-off":62346,"music-note-plus":64954,"music-note-quarter":62347,"music-note-quarter-dotted":65426,"music-note-sixteenth":62348,"music-note-sixteenth-dotted":65427,"music-note-whole":62349,"music-note-whole-dotted":65428,"music-off":63322,"music-rest-eighth":65429,"music-rest-half":65430,"music-rest-quarter":65431,"music-rest-sixteenth":65432,"music-rest-whole":65433,"nail":64955,"nas":63730,"nature":62350,"nature-people":62351,"navigation":62352,"near-me":62925,"necklace":65320,"needle":62353,"network":63218,"network-off":64631,"network-strength-1":63731,"network-strength-1-alert":63732,"network-strength-2":63733,"network-strength-2-alert":63734,"network-strength-3":63735,"network-strength-3-alert":63736,"network-strength-4":63737,"network-strength-4-alert":63738,"network-strength-off":63739,"new-box":62356,"newspaper":62357,"newspaper-minus":65321,"newspaper-plus":65322,"newspaper-variant":983075,"newspaper-variant-multiple":983076,"nfc-search-variant":65078,"nfc-tap":62359,"nfc-variant":62360,"nfc-variant-off":65079,"ninja":63347,"nintendo-game-boy":988051,"noodles":983465,"not-equal":63884,"not-equal-variant":63885,"note":62362,"note-multiple":63159,"note-plus":62364,"note-text":62366,"notebook":63533,"notebook-multiple":65080,"notification-clear-all":62367,"nuke":63139,"null":63457,"numeric":62368,"numeric-0":48,"numeric-0-box":62369,"numeric-0-box-multiple":65323,"numeric-0-circle":64634,"numeric-1":49,"numeric-1-box":62372,"numeric-1-box-multiple":65324,"numeric-1-circle":64636,"numeric-10":983050,"numeric-10-box":65434,"numeric-10-box-multiple":983051,"numeric-10-circle":983053,"numeric-2":50,"numeric-2-box":62375,"numeric-2-box-multiple":65325,"numeric-2-circle":64638,"numeric-3":51,"numeric-3-box":62378,"numeric-3-box-multiple":65326,"numeric-3-circle":64640,"numeric-4":52,"numeric-4-box":62381,"numeric-4-box-multiple":65327,"numeric-4-circle":64642,"numeric-5":53,"numeric-5-box":62384,"numeric-5-box-multiple":65328,"numeric-5-circle":64644,"numeric-6":54,"numeric-6-box":62387,"numeric-6-box-multiple":65329,"numeric-6-circle":64646,"numeric-7":55,"numeric-7-box":62390,"numeric-7-box-multiple":65330,"numeric-7-circle":64648,"numeric-8":56,"numeric-8-box":62393,"numeric-8-box-multiple":65331,"numeric-8-circle":64650,"numeric-9":57,"numeric-9-box":62396,"numeric-9-box-multiple":65332,"numeric-9-circle":64652,"numeric-9-plus":983055,"numeric-9-plus-box":62399,"numeric-9-plus-box-multiple":65333,"numeric-9-plus-circle":64654,"numeric-negative-1":983156,"nut":63223,"nutrition":62402,"oar":63099,"ocarina":64956,"ocr":983397,"octagon":62403,"octagram":63224,"offer":983622,"office-building":63888,"oil":62407,"oil-lamp":65334,"oil-level":983157,"oil-temperature":983065,"om":63858,"omega":62409,"one-up":64393,"opacity":62924,"open-in-app":62411,"open-in-new":62412,"orbit":61464,"order-alphabetical-ascending":11299,"order-alphabetical-descending":986375,"order-bool-ascending":983742,"order-bool-ascending-variant":985487,"order-bool-descending":988036,"order-bool-descending-variant":985488,"order-numeric-ascending":984389,"order-numeric-descending":984390,"ornament":62415,"ornament-variant":62416,"outdoor-lamp":983158,"overscan":983079,"owl":62418,"pac-man":64395,"package":62419,"package-down":62420,"package-up":62421,"package-variant":62422,"package-variant-closed":62423,"page-first":62976,"page-last":62977,"page-layout-body":63225,"page-layout-footer":63226,"page-layout-header":63227,"page-layout-header-footer":65436,"page-layout-sidebar-left":63228,"page-layout-sidebar-right":63229,"page-next":64396,"page-previous":64398,"pail":988183,"pail-minus":988215,"pail-off":988217,"pail-plus":988214,"pail-remove":988216,"palette":62424,"palette-advanced":62425,"palette-swatch":63668,"palm-tree":983159,"pan":64400,"pan-bottom-left":64401,"pan-bottom-right":64402,"pan-down":64403,"pan-horizontal":64404,"pan-left":64405,"pan-right":64406,"pan-top-left":64407,"pan-top-right":64408,"pan-up":64409,"pan-vertical":64410,"panda":62426,"panorama":62428,"panorama-fisheye":62429,"panorama-horizontal-outline":62430,"panorama-vertical-outline":62431,"panorama-wide-angle-outline":62432,"paper-cut-vertical":62433,"paper-roll":983426,"paperclip":62434,"parachute":64656,"parking":62435,"party-popper":983160,"passport":63458,"passport-biometric":64957,"pasta":983435,"patio-heater":65437,"pause":62436,"pause-box":61628,"pause-circle":62437,"pause-octagon":62439,"paw":62441,"paw-off":63063,"peace":63619,"peanut":983070,"peanut-off":983071,"pen":62442,"pen-lock":64958,"pen-minus":64959,"pen-off":64960,"pen-plus":64961,"pen-remove":64962,"pencil":62443,"pencil-box":62444,"pencil-box-multiple":983407,"pencil-circle":63230,"pencil-lock":62446,"pencil-minus":64964,"pencil-off":62447,"pencil-plus":64967,"pencil-remove":64969,"pencil-ruler":987987,"penguin":65245,"pentagon":63231,"percent":62448,"periodic-table":63669,"perspective-less":64767,"perspective-more":64768,"phone":62450,"phone-alert":65335,"phone-bluetooth":62451,"phone-cancel":983271,"phone-check":983508,"phone-classic":62978,"phone-classic-off":983716,"phone-forward":62452,"phone-hangup":62453,"phone-in-talk":62454,"phone-incoming":62455,"phone-lock":62456,"phone-log":62457,"phone-message":983489,"phone-minus":63064,"phone-missed":62458,"phone-off":64971,"phone-outgoing":62459,"phone-paused":62460,"phone-plus":63065,"phone-return":63534,"phone-ring":983510,"phone-rotate-landscape":63620,"phone-rotate-portrait":63621,"phone-settings":62461,"phone-voip":62462,"pi":62463,"pi-box":62464,"piano":63100,"pickaxe":63670,"picture-in-picture-bottom-right":65082,"picture-in-picture-top-right":65084,"pier":63622,"pier-crane":63623,"pig":62465,"pig-variant":983080,"piggy-bank":983081,"pill":62466,"pillar":63233,"pin":62467,"pin-off":62468,"pine-tree":62469,"pine-tree-box":62470,"pine-tree-fire":988186,"pinwheel":64212,"pipe":63460,"pipe-disconnected":63461,"pipe-leak":63624,"pipe-wrench":987988,"pirate":64007,"pistol":63234,"piston":63625,"pizza":62473,"play":62474,"play-box":983717,"play-box-multiple":64757,"play-circle":62476,"play-network":63626,"play-pause":62478,"play-protected-content":62479,"play-speed":63742,"playlist-check":62919,"playlist-edit":63743,"playlist-minus":62480,"playlist-music":64660,"playlist-play":62481,"playlist-plus":62482,"playlist-remove":62483,"playlist-star":64974,"plus":62485,"plus-box":62486,"plus-box-multiple":62260,"plus-circle":62487,"plus-circle-multiple":983884,"plus-minus":63889,"plus-minus-box":63890,"plus-minus-variant":988361,"plus-network":62490,"plus-thick":983575,"podcast":63891,"podium":64769,"podium-bronze":64770,"podium-gold":64771,"podium-silver":64772,"point-of-sale":64878,"poker-chip":63535,"polaroid":62494,"police-badge":983442,"poll":62495,"polo":988355,"pool":62982,"popcorn":62498,"post":983082,"postage-stamp":64663,"pot":15975,"pot-mix":63067,"pot-steam":63066,"pound":62499,"pound-box":62500,"power":62501,"power-cycle":63744,"power-off":63745,"power-on":63746,"power-plug":63140,"power-plug-off":63141,"power-settings":62502,"power-sleep":63747,"power-socket":62503,"power-socket-au":63748,"power-socket-de":983346,"power-socket-eu":63462,"power-socket-fr":983347,"power-socket-jp":983348,"power-socket-uk":63463,"power-socket-us":63464,"power-standby":63749,"prescription":63237,"presentation":62504,"presentation-play":62505,"printer":62506,"printer-3d":62507,"printer-3d-nozzle":65086,"printer-3d-nozzle-alert":983531,"printer-alert":62508,"printer-check":983409,"printer-eye":988248,"printer-off":65088,"printer-pos":983161,"printer-search":988247,"printer-settings":63238,"printer-wireless":64010,"priority-high":62979,"priority-low":62980,"professional-hexagon":62509,"progress-alert":64664,"progress-check":63892,"progress-clock":63893,"progress-close":983349,"progress-download":63894,"progress-upload":63895,"progress-wrench":64665,"projector":62510,"projector-screen":62511,"propane-tank":987991,"protocol":65529,"publish":63142,"pulse":62512,"pump":988162,"pumpkin":64411,"purse":65337,"puzzle":62513,"puzzle-check":988198,"puzzle-edit":988371,"puzzle-heart":988372,"puzzle-minus":988369,"puzzle-plus":988368,"puzzle-remove":988370,"puzzle-star":988373,"qrcode":62514,"qrcode-edit":63671,"qrcode-minus":983479,"qrcode-plus":983478,"qrcode-remove":983480,"qrcode-scan":62515,"quadcopter":62516,"quality-high":62517,"quality-low":64011,"quality-medium":64012,"rabbit":63750,"racing-helmet":64879,"racquetball":64880,"radar":62519,"radiator":62520,"radiator-disabled":64214,"radiator-off":64215,"radio":62521,"radio-am":64666,"radio-fm":64667,"radio-handheld":62522,"radio-off":983623,"radio-tower":62523,"radioactive":62524,"radioactive-off":65246,"radiobox-blank":62525,"radiobox-marked":62526,"radiology-box":988357,"radius":64668,"railroad-light":65339,"raspberry-pi":62527,"ray-end":62528,"ray-end-arrow":62529,"ray-start":62530,"ray-start-arrow":62531,"ray-start-end":62532,"ray-vertex":62533,"read":62535,"receipt":63523,"receipt-outline":62711,"receipt-text":62537,"record":62538,"record-circle":65247,"record-player":63897,"record-rec":62539,"rectangle":65089,"recycle":62540,"recycle-variant":988061,"redo":62542,"redo-variant":62543,"reflect-horizontal":64013,"reflect-vertical":64014,"refresh":62544,"refresh-circle":988023,"regex":62545,"registered-trademark":64102,"relation-many-to-many":988310,"relation-many-to-one":988311,"relation-many-to-one-or-many":988312,"relation-many-to-only-one":988313,"relation-many-to-zero-or-many":988314,"relation-many-to-zero-or-one":988315,"relation-one-or-many-to-many":988316,"relation-one-or-many-to-one":988317,"relation-one-or-many-to-one-or-many":988318,"relation-one-or-many-to-only-one":988319,"relation-one-or-many-to-zero-or-many":988320,"relation-one-or-many-to-zero-or-one":988321,"relation-one-to-many":988322,"relation-one-to-one":988323,"relation-one-to-one-or-many":988324,"relation-one-to-only-one":988325,"relation-one-to-zero-or-many":988326,"relation-one-to-zero-or-one":988327,"relation-only-one-to-many":988328,"relation-only-one-to-one":988329,"relation-only-one-to-one-or-many":988330,"relation-only-one-to-only-one":988331,"relation-only-one-to-zero-or-many":988332,"relation-only-one-to-zero-or-one":988333,"relation-zero-or-many-to-many":988334,"relation-zero-or-many-to-one":988335,"relation-zero-or-many-to-one-or-many":988336,"relation-zero-or-many-to-only-one":988337,"relation-zero-or-many-to-zero-or-many":988338,"relation-zero-or-many-to-zero-or-one":988339,"relation-zero-or-one-to-many":988340,"relation-zero-or-one-to-one":988341,"relation-zero-or-one-to-one-or-many":988342,"relation-zero-or-one-to-only-one":988343,"relation-zero-or-one-to-zero-or-many":988344,"relation-zero-or-one-to-zero-or-one":988345,"relative-scale":62546,"reload":62547,"reload-alert":983350,"reminder":63627,"remote":62548,"remote-desktop":63672,"remote-off":65249,"remote-tv":65250,"remote-tv-off":65251,"rename-box":62549,"reorder-horizontal":63111,"reorder-vertical":63112,"repeat":62550,"repeat-off":62551,"repeat-once":62552,"repeat-variant":62791,"replay":62553,"reply":62554,"reply-all":62555,"reply-circle":983513,"reproduction":62556,"resistor":64287,"resistor-nodes":64288,"resize":64103,"resize-bottom-right":62557,"responsive":62558,"restart":63240,"restart-alert":983351,"restart-off":64881,"restore":63898,"restore-alert":983352,"rewind":62559,"rewind-10":64774,"rewind-30":64882,"rewind-5":983588,"rhombus":63242,"rhombus-medium":64015,"rhombus-split":64016,"ribbon":62560,"rice":63465,"ring":63466,"rivet":65091,"road":62561,"road-variant":62562,"robber":983162,"robot":63144,"robot-industrial":64289,"robot-mower":983586,"robot-vacuum":63244,"robot-vacuum-variant":63751,"rocket":62563,"rocket-launch":988382,"rodent":987943,"roller-skate":64775,"roller-skate-off":983365,"rollerblade":64776,"rollerblade-off":22033,"roman-numeral-1":983219,"roman-numeral-10":983228,"roman-numeral-2":983220,"roman-numeral-3":983221,"roman-numeral-4":983222,"roman-numeral-5":983223,"roman-numeral-6":983224,"roman-numeral-7":983225,"roman-numeral-8":983226,"roman-numeral-9":983227,"room-service":63628,"rotate-3d":65252,"rotate-3d-variant":62564,"rotate-left":62565,"rotate-left-variant":62566,"rotate-orbit":64884,"rotate-right":62567,"rotate-right-variant":62568,"rounded-corner":62983,"router":983565,"router-network":983218,"router-wireless":62569,"router-wireless-settings":64104,"routes":62570,"routes-clock":983163,"rowing":62984,"rss":62571,"rss-box":62572,"rss-off":65342,"rug":988277,"rugby":64885,"ruler":62573,"ruler-square":64670,"ruler-square-compass":65243,"run":63245,"run-fast":62574,"rv-truck":983551,"sack":64778,"sack-percent":64779,"safe":64105,"safe-square":983719,"safety-goggles":64780,"sail-boat":65253,"sale":62575,"satellite":62576,"satellite-uplink":63752,"satellite-variant":62577,"sausage":63673,"saw-blade":65092,"sawtooth-wave":988282,"saxophone":62985,"scale":62578,"scale-balance":62929,"scale-bathroom":62579,"scale-off":983164,"scan-helper":988120,"scanner":63146,"scanner-off":63753,"scatter-plot":65254,"school":62580,"scissors-cutting":64106,"scoreboard":983721,"screen-rotation":62581,"screen-rotation-lock":62582,"screw-flat-top":64975,"screw-lag":65108,"screw-machine-flat-top":65109,"screw-machine-round-top":65110,"screw-round-top":65111,"screwdriver":62583,"script":64413,"script-text":64414,"sd":62585,"seal":62586,"seal-variant":65530,"search-web":63246,"seat":64671,"seat-flat":62587,"seat-flat-angled":62588,"seat-individual-suite":62589,"seat-legroom-extra":62590,"seat-legroom-normal":62591,"seat-legroom-reduced":62592,"seat-passenger":983668,"seat-recline-extra":62593,"seat-recline-normal":62594,"seatbelt":64673,"security":62595,"security-network":62596,"seed":65093,"seed-off":988157,"segment":65256,"select":62597,"select-all":62598,"select-color":64781,"select-compare":64216,"select-drag":64107,"select-group":65439,"select-inverse":62599,"select-marker":983723,"select-multiple":983724,"select-multiple-marker":983725,"select-off":62600,"select-place":65531,"select-search":983599,"selection":62601,"selection-drag":64108,"selection-ellipse":64782,"selection-ellipse-arrow-inside":65343,"selection-marker":983726,"selection-multiple":983728,"selection-multiple-marker":983727,"selection-off":63350,"selection-search":983600,"send":62602,"send-check":983436,"send-circle":65112,"send-clock":983438,"send-lock":63468,"serial-port":63068,"server":62603,"server-minus":62604,"server-network":62605,"server-network-off":62606,"server-off":62607,"server-plus":62608,"server-remove":62609,"server-security":62610,"set-all":63351,"set-center":63352,"set-center-right":63353,"set-left":63354,"set-left-center":63355,"set-left-right":63356,"set-merge":988384,"set-none":63357,"set-right":63358,"set-split":988385,"set-square":988253,"set-top-box":63902,"settings-helper":64109,"shaker":983353,"shape":63536,"shape-circle-plus":63069,"shape-oval-plus":983589,"shape-plus":62613,"shape-polygon-plus":63070,"shape-rectangle-plus":63071,"shape-square-plus":63072,"share":62614,"share-all":983583,"share-circle":983512,"share-off":65344,"share-variant":62615,"sheep":64674,"shield":62616,"shield-account":63630,"shield-airplane":63162,"shield-alert":65257,"shield-bug":988122,"shield-car":65440,"shield-check":62821,"shield-cross":64677,"shield-edit":983499,"shield-half":988000,"shield-half-full":63359,"shield-home":63113,"shield-key":64416,"shield-link-variant":64783,"shield-lock":63900,"shield-off":63901,"shield-plus":64217,"shield-refresh":12326,"shield-remove":64219,"shield-search":64886,"shield-star":983398,"shield-sun":983167,"shield-sync":983501,"ship-wheel":63538,"shoe-formal":64290,"shoe-heel":64291,"shoe-print":65114,"shopping":62618,"shopping-music":62619,"shopping-search":65441,"shovel":63247,"shovel-off":63248,"shower":63903,"shower-head":63904,"shredder":62620,"shuffle":62621,"shuffle-disabled":62622,"shuffle-variant":62623,"shuriken":988031,"sigma":62624,"sigma-lower":63019,"sign-caution":62625,"sign-direction":63360,"sign-direction-minus":983074,"sign-direction-plus":65533,"sign-direction-remove":65534,"sign-real-estate":983363,"sign-text":63361,"signal":62626,"signal-2g":63249,"signal-3g":63250,"signal-4g":63251,"signal-5g":64110,"signal-cellular-1":63675,"signal-cellular-2":63676,"signal-cellular-3":63677,"signal-distance-variant":65095,"signal-hspa":63252,"signal-hspa-plus":63253,"signal-off":63362,"signal-variant":62986,"signature":65115,"signature-freehand":65116,"signature-image":65117,"signature-text":65118,"silo-outline":64292,"silverware":62627,"silverware-clean":65535,"silverware-fork":62628,"silverware-fork-knife":64111,"silverware-spoon":62629,"silverware-variant":62630,"sim":62631,"sim-alert":62632,"sim-off":62633,"sine-wave":63834,"sitemap":62634,"size-l":988070,"size-m":988069,"size-s":988068,"size-xl":988071,"size-xs":988067,"size-xxl":988072,"size-xxs":988066,"size-xxxl":988073,"skate":64785,"skateboard":988354,"skew-less":64786,"skew-more":64787,"ski":983855,"ski-cross-country":983856,"ski-water":983857,"skip-backward":62635,"skip-forward":62636,"skip-next":62637,"skip-next-circle":63073,"skip-previous":62638,"skip-previous-circle":63075,"skull":63115,"skull-crossbones":64418,"skull-scan":988359,"slash-forward":983040,"slash-forward-box":983041,"sledding":62491,"sleep":62642,"sleep-off":62643,"slope-downhill":65119,"slope-uphill":65120,"slot-machine":983359,"smart-card":983272,"smart-card-reader":983274,"smog":64112,"smoke-detector":62354,"smoking":62644,"smoking-off":62645,"smoking-pipe":988173,"smoking-pipe-off":988200,"snowboard":983858,"snowflake":63254,"snowflake-alert":65350,"snowflake-melt":983798,"snowflake-variant":65351,"snowman":62647,"soccer":62648,"soccer-field":63539,"sofa":62649,"solar-panel":64887,"solar-panel-large":64888,"solar-power":64113,"soldering-iron":983229,"solid":63116,"sort":62650,"sort-alphabetical-ascending":984509,"sort-alphabetical-ascending-variant":983411,"sort-alphabetical-descending":984511,"sort-alphabetical-descending-variant":983412,"sort-alphabetical-variant":62651,"sort-ascending":62652,"sort-bool-ascending":988037,"sort-bool-ascending-variant":988038,"sort-bool-descending":988039,"sort-bool-descending-variant":988040,"sort-descending":62653,"sort-numeric-ascending":988041,"sort-numeric-ascending-variant":985357,"sort-numeric-descending":988042,"sort-numeric-descending-variant":985810,"sort-numeric-variant":62654,"sort-reverse-variant":4420,"sort-variant":62655,"sort-variant-lock":64681,"sort-variant-lock-open":64682,"sort-variant-remove":983410,"source-branch":63020,"source-branch-check":988367,"source-branch-minus":988363,"source-branch-plus":988362,"source-branch-refresh":988365,"source-branch-remove":988364,"source-branch-sync":988366,"source-commit":63255,"source-commit-end":63256,"source-commit-end-local":63257,"source-commit-local":63258,"source-commit-next-local":63259,"source-commit-start":63260,"source-commit-start-next-local":63261,"source-fork":62657,"source-merge":63021,"source-pull":62658,"source-repository":64683,"source-repository-multiple":64684,"soy-sauce":63469,"soy-sauce-off":988156,"spa":64685,"space-invaders":64421,"space-station":988035,"spade":65096,"speaker":62659,"speaker-bluetooth":63905,"speaker-multiple":64788,"speaker-off":62660,"speaker-wireless":63262,"speedometer":62661,"speedometer-medium":65442,"speedometer-slow":65443,"spellcheck":62662,"spider":983573,"spider-thread":983574,"spider-web":64422,"spoon-sugar":988201,"spotlight":62664,"spotlight-beam":62665,"spray":63077,"spray-bottle":64223,"sprinkler":983169,"sprinkler-variant":983170,"sprout":65097,"square":63331,"square-medium":64018,"square-off":983833,"square-root":63363,"square-root-box":63906,"square-small":64020,"square-wave":988283,"squeegee":64224,"ssh":63679,"stadium":983066,"stadium-variant":63263,"stairs":62669,"stairs-box":988062,"stairs-down":983785,"stairs-up":983784,"stamper":64789,"standard-definition":63470,"star":62670,"star-box":64114,"star-box-multiple":983729,"star-circle":62671,"star-crescent":63864,"star-david":63865,"star-face":63908,"star-four-points":64225,"star-half":19991,"star-half-full":62672,"star-off":62673,"star-three-points":64227,"state-machine":983578,"steering":62676,"steering-off":63757,"step-backward":62677,"step-backward-2":62678,"step-forward":62679,"step-forward-2":62680,"stethoscope":62681,"sticker":988004,"sticker-alert":988005,"sticker-check":988007,"sticker-emoji":63364,"sticker-minus":988009,"sticker-plus":988012,"sticker-remove":988014,"stocking":62682,"stomach":983230,"stop":62683,"stop-circle":63078,"store":62684,"store-24-hour":62685,"storefront":985031,"stove":62686,"strategy":983553,"stretch-to-page":65352,"string-lights":983781,"string-lights-off":983782,"subdirectory-arrow-left":62988,"subdirectory-arrow-right":62989,"subtitles":64021,"subway":63147,"subway-alert-variant":64889,"subway-variant":62687,"summit":63365,"sunglasses":62688,"surround-sound":62917,"surround-sound-2-0":63471,"surround-sound-3-1":63472,"surround-sound-5-1":63473,"surround-sound-7-1":63474,"swap-horizontal":62689,"swap-horizontal-bold":64425,"swap-horizontal-circle":983042,"swap-horizontal-variant":63680,"swap-vertical":62690,"swap-vertical-bold":64426,"swap-vertical-circle":983044,"swap-vertical-variant":63681,"swim":62691,"switch":62692,"sword":62693,"sword-cross":63366,"syllabary-hangul":987955,"syllabary-hiragana":987956,"syllabary-katakana":987957,"syllabary-katakana-halfwidth":987958,"sync":62694,"sync-alert":62695,"sync-circle":988024,"sync-off":62696,"tab":62697,"tab-minus":64294,"tab-plus":63323,"tab-remove":64295,"tab-unselected":62698,"table":62699,"table-account":988089,"table-alert":988090,"table-arrow-down":988091,"table-arrow-left":988092,"table-arrow-right":988093,"table-arrow-up":988094,"table-border":64023,"table-cancel":988095,"table-chair":983171,"table-check":988096,"table-clock":988097,"table-cog":988098,"table-column":63540,"table-column-plus-after":62700,"table-column-plus-before":62701,"table-column-remove":62702,"table-column-width":62703,"table-edit":62704,"table-eye":983231,"table-eye-off":988099,"table-furniture":984508,"table-headers-eye":983624,"table-headers-eye-off":983625,"table-heart":988100,"table-key":988101,"table-large":62705,"table-large-plus":65444,"table-large-remove":65445,"table-lock":988102,"table-merge-cells":63909,"table-minus":988103,"table-multiple":988104,"table-network":988105,"table-of-contents":63541,"table-off":988106,"table-plus":64116,"table-refresh":988064,"table-remove":64117,"table-row":63542,"table-row-height":62706,"table-row-plus-after":62707,"table-row-plus-before":62708,"table-row-remove":62709,"table-search":63758,"table-settings":63543,"table-split-cell":988202,"table-star":988107,"table-sync":988065,"table-tennis":65099,"tablet":62710,"tablet-cellphone":63910,"tablet-dashboard":65259,"taco":63329,"tag":62713,"tag-faces":62714,"tag-heart":63114,"tag-minus":63759,"tag-multiple":62715,"tag-off":983627,"tag-plus":63265,"tag-remove":63266,"tag-text":983631,"tangram":62712,"tank":64790,"tanker-truck":983046,"tape-measure":64296,"target":62718,"target-account":64428,"target-variant":64118,"taxi":62719,"tea":64890,"telescope":64297,"television":62722,"television-ambient-light":987990,"television-box":63544,"television-classic":63475,"television-classic-off":63545,"television-guide":62723,"television-off":63546,"television-pause":65446,"television-play":65260,"television-shimmer":983355,"television-stop":65447,"temperature-celsius":62724,"temperature-fahrenheit":62725,"temperature-kelvin":62726,"tennis":64892,"tennis-ball":62727,"tent":62728,"terrain":62729,"test-tube":63080,"test-tube-empty":63760,"test-tube-off":63761,"text":63911,"text-box":61978,"text-box-check":65219,"text-box-minus":65221,"text-box-multiple":64182,"text-box-plus":65223,"text-box-remove":65225,"text-box-search":65227,"text-long":63913,"text-recognition":983400,"text-search":988088,"text-shadow":63081,"text-short":63912,"texture":62732,"texture-box":983047,"theater":62733,"theme-light-dark":62734,"thermometer":62735,"thermometer-alert":65121,"thermometer-chevron-down":65122,"thermometer-chevron-up":65123,"thermometer-high":983277,"thermometer-lines":62736,"thermometer-low":983278,"thermometer-minus":65124,"thermometer-plus":65125,"thermostat":62355,"thermostat-box":63632,"thought-bubble":63477,"thumb-down":62737,"thumb-up":62739,"thumbs-up-down":62741,"ticket":62742,"ticket-account":62743,"ticket-confirmation":62744,"ticket-percent":63267,"tie":62745,"tilde":63268,"timelapse":62746,"timeline":64429,"timeline-alert":65458,"timeline-clock":983590,"timeline-plus":65459,"timeline-question":65462,"timeline-text":64431,"timer":988075,"timer-10":62748,"timer-3":62749,"timer-off":988076,"timer-sand":62751,"timer-sand-empty":63148,"timer-sand-full":63371,"timetable":62752,"toaster":983173,"toaster-off":983522,"toaster-oven":64687,"toggle-switch":62753,"toggle-switch-off":62754,"toilet":63914,"toolbox":63915,"tools":983174,"tooltip":62755,"tooltip-account":61452,"tooltip-edit":62756,"tooltip-image":62757,"tooltip-plus":64434,"tooltip-text":62760,"tooth":63682,"toothbrush":983380,"toothbrush-electric":983383,"toothbrush-paste":983381,"tortoise":64791,"toslink":983779,"tournament":63917,"tow-truck":63547,"tower-beach":63104,"tower-fire":63105,"toy-brick":983731,"toy-brick-marker":983732,"toy-brick-minus":983734,"toy-brick-plus":983737,"toy-brick-remove":983739,"toy-brick-search":983741,"track-light":63763,"trackpad":63479,"trackpad-lock":63794,"tractor":63633,"tractor-variant":988356,"trademark":64119,"traffic-cone":988028,"traffic-light":62763,"train":62764,"train-car":64436,"train-variant":63683,"tram":62765,"tram-side":983048,"transcribe":62766,"transcribe-close":62767,"transfer":983175,"transfer-down":64893,"transfer-left":64894,"transfer-right":62768,"transfer-up":64895,"transit-connection":64792,"transit-connection-variant":64793,"transit-detour":65448,"transit-transfer":63149,"transition":63764,"transition-masked":63765,"translate":62922,"translate-off":65126,"transmission-tower":64794,"trash-can":64120,"tray":983743,"tray-alert":983744,"tray-arrow-down":61728,"tray-arrow-up":61725,"tray-full":983745,"tray-minus":983746,"tray-plus":983747,"tray-remove":983748,"treasure-chest":63269,"tree":62769,"trending-down":62771,"trending-neutral":62772,"trending-up":62773,"triangle":62774,"triangle-wave":988284,"triforce":64437,"trophy":62776,"trophy-award":62777,"trophy-broken":64896,"trophy-variant":62779,"truck":62781,"truck-check":64688,"truck-delivery":62782,"truck-fast":63367,"truck-trailer":63270,"trumpet":983233,"tshirt-crew":64122,"tshirt-v":64123,"tumble-dryer":63766,"tumble-dryer-alert":983525,"tumble-dryer-off":983526,"tune":63022,"tune-vertical":63082,"turnstile":64689,"turtle":64691,"two-factor-authentication":63918,"typewriter":65354,"ufo":983279,"ultra-high-definition":63480,"umbrella":62794,"umbrella-closed":63919,"umbrella-closed-variant":988129,"undo":62796,"undo-variant":62797,"unfold-less-horizontal":62798,"unfold-less-vertical":63327,"unfold-more-horizontal":62799,"unfold-more-vertical":63328,"ungroup":62800,"update":63151,"upload":62802,"upload-lock":988019,"upload-multiple":63548,"upload-network":63221,"upload-off":983281,"usb":62803,"usb-flash-drive":983753,"usb-port":983579,"valve":983176,"valve-closed":983177,"valve-open":983178,"van-passenger":63481,"van-utility":63482,"vanish":63483,"vanity-light":983564,"variable":64230,"variable-box":983356,"vector-arrange-above":62804,"vector-arrange-below":62805,"vector-bezier":64231,"vector-circle":62806,"vector-circle-variant":62807,"vector-combine":62808,"vector-curve":62809,"vector-difference":62810,"vector-difference-ab":62811,"vector-difference-ba":62812,"vector-ellipse":63634,"vector-intersection":62813,"vector-line":62814,"vector-link":983049,"vector-point":61892,"vector-point-edit":63975,"vector-point-select":62815,"vector-polygon":62816,"vector-polyline":62817,"vector-polyline-edit":983632,"vector-polyline-minus":983633,"vector-polyline-plus":983634,"vector-polyline-remove":983635,"vector-radius":63305,"vector-rectangle":62918,"vector-selection":62818,"vector-triangle":62819,"vector-union":62820,"vhs":64026,"vibrate":62822,"vibrate-off":64693,"video":62823,"video-3d":63484,"video-3d-off":988121,"video-3d-variant":65262,"video-4k-box":63549,"video-account":63768,"video-box":61693,"video-box-off":61694,"video-check":983179,"video-image":63769,"video-input-antenna":63550,"video-input-component":63551,"video-input-hdmi":63552,"video-input-scart":65449,"video-input-svideo":63553,"video-minus":63921,"video-off":62824,"video-plus":63922,"video-stabilization":63770,"video-switch":62825,"video-vintage":64027,"video-wireless":65263,"view-agenda":62826,"view-array":62827,"view-carousel":62828,"view-column":62829,"view-comfy":65101,"view-compact":65102,"view-dashboard":62830,"view-dashboard-variant":63554,"view-day":62831,"view-grid":62832,"view-grid-plus":65450,"view-headline":62833,"view-list":62834,"view-module":62835,"view-parallel":63271,"view-quilt":62836,"view-sequential":63272,"view-split-horizontal":64423,"view-split-vertical":64424,"view-stream":62837,"view-week":62838,"violin":62991,"virtual-reality":63635,"virus":988086,"voicemail":62845,"volleyball":63923,"volume-high":62846,"volume-low":62847,"volume-medium":62848,"volume-minus":63325,"volume-mute":63326,"volume-off":62849,"volume-plus":63324,"volume-source":983371,"volume-variant-off":65128,"volume-vibrate":983372,"vote":64030,"vpn":62850,"walk":62851,"wall":63485,"wall-sconce":63771,"wall-sconce-flat":63772,"wall-sconce-flat-variant":984092,"wall-sconce-round":984904,"wall-sconce-round-variant":63773,"wallet":62852,"wallet-giftcard":62853,"wallet-membership":62854,"wallet-plus":65451,"wallet-travel":62855,"wallpaper":65129,"wan":62856,"wardrobe":65453,"warehouse":65467,"washing-machine":63273,"washing-machine-alert":983527,"washing-machine-off":983528,"watch":62857,"watch-export":62858,"watch-export-variant":63636,"watch-import":62859,"watch-import-variant":63637,"watch-variant":63638,"watch-vibrate":63152,"watch-vibrate-off":64694,"water":62860,"water-boiler":65455,"water-boiler-alert":983518,"water-boiler-off":983519,"water-off":62861,"water-percent":62862,"water-polo":983755,"water-pump":62863,"water-pump-off":65456,"water-well":983181,"watering-can":988289,"watermark":62994,"wave":65355,"waveform":988285,"waves":63372,"weather-cloudy":62864,"weather-cloudy-alert":65356,"weather-cloudy-arrow-right":65105,"weather-fog":62865,"weather-hail":62866,"weather-hazy":65357,"weather-hurricane":63639,"weather-lightning":62867,"weather-lightning-rainy":63101,"weather-night":62868,"weather-night-partly-cloudy":65358,"weather-partly-cloudy":62869,"weather-partly-lightning":65359,"weather-partly-rainy":65360,"weather-partly-snowy":65361,"weather-partly-snowy-rainy":65362,"weather-pouring":62870,"weather-rainy":62871,"weather-snowy":62872,"weather-snowy-heavy":65363,"weather-snowy-rainy":63102,"weather-sunny":62873,"weather-sunny-alert":65364,"weather-sunset":62874,"weather-sunset-down":62875,"weather-sunset-up":62876,"weather-tornado":65365,"weather-windy":62877,"weather-windy-variant":62878,"web":62879,"web-box":65457,"web-clock":983669,"webcam":62880,"webhook":63023,"weight":62881,"weight-gram":64795,"weight-kilogram":62882,"weight-lifter":983432,"weight-pound":63924,"wheelchair-accessibility":62884,"whistle":63925,"white-balance-auto":62885,"white-balance-incandescent":62886,"white-balance-iridescent":62887,"white-balance-sunny":62888,"widgets":63275,"wifi":62889,"wifi-off":62890,"wifi-star":65131,"wifi-strength-1":63774,"wifi-strength-1-alert":63775,"wifi-strength-1-lock":63776,"wifi-strength-2":63777,"wifi-strength-2-alert":63778,"wifi-strength-2-lock":63779,"wifi-strength-3":63780,"wifi-strength-3-alert":63781,"wifi-strength-3-lock":63782,"wifi-strength-4":63783,"wifi-strength-4-alert":63784,"wifi-strength-4-lock":63785,"wifi-strength-off":63788,"wind-turbine":64897,"window-close":62893,"window-closed":62894,"window-closed-variant":983558,"window-maximize":62895,"window-minimize":62896,"window-open":62897,"window-open-variant":983559,"window-restore":62898,"window-shutter":983367,"window-shutter-alert":983368,"window-shutter-open":983369,"wiper":64232,"wiper-wash":64898,"wizard-hat":988279,"wrap":62902,"wrap-disabled":64443,"wrench":62903,"xml":62912,"yeast":62913,"yin-yang":63103,"yoga":983463,"youtube-subscription":64796,"zip-box":62916,"zip-disk":64034,"zodiac-aquarius":64124,"zodiac-aries":64125,"zodiac-cancer":64126,"zodiac-capricorn":64127,"zodiac-gemini":64128,"zodiac-leo":64129,"zodiac-libra":64130,"zodiac-pisces":64131,"zodiac-sagittarius":64132,"zodiac-scorpio":64133,"zodiac-taurus":64134,"zodiac-virgo":64135} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/qs/TileExtensions.kt b/app/src/main/java/io/homeassistant/companion/android/qs/TileExtensions.kt index abd7b3f81..be307b411 100755 --- a/app/src/main/java/io/homeassistant/companion/android/qs/TileExtensions.kt +++ b/app/src/main/java/io/homeassistant/companion/android/qs/TileExtensions.kt @@ -13,12 +13,8 @@ import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.content.getSystemService -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.graphics.drawable.toBitmap -import com.maltaisn.icondialog.pack.IconPack -import com.maltaisn.icondialog.pack.IconPackLoader -import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.sizeDp import dagger.hilt.EntryPoint import dagger.hilt.InstallIn @@ -35,6 +31,7 @@ import io.homeassistant.companion.android.database.qs.isSetup import io.homeassistant.companion.android.database.qs.numberedId import io.homeassistant.companion.android.settings.SettingsActivity import io.homeassistant.companion.android.settings.qs.updateActiveTileServices +import io.homeassistant.companion.android.util.icondialog.getIconByMdiName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope @@ -113,7 +110,7 @@ abstract class TileExtensions : TileService() { serverManager.integrationRepository(tileData.serverId).getEntityUpdates(listOf(tileData.entityId))?.collect { tile.state = if (it.state in validActiveStates) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE - getTileIcon(tileData.iconId, it, applicationContext)?.let { icon -> + getTileIcon(tileData.iconName, it, applicationContext)?.let { icon -> tile.icon = Icon.createWithBitmap(icon) } tile.updateTile() @@ -147,7 +144,7 @@ abstract class TileExtensions : TileService() { val state: Entity<*>? = if ( tileData.entityId.split(".")[0] in toggleDomainsWithLock || - tileData.iconId == null + tileData.iconName == null ) { withContext(Dispatchers.IO) { try { @@ -170,7 +167,7 @@ abstract class TileExtensions : TileService() { tile.state = Tile.STATE_INACTIVE } - getTileIcon(tileData.iconId, state, context)?.let { icon -> + getTileIcon(tileData.iconName, state, context)?.let { icon -> tile.icon = Icon.createWithBitmap(icon) } Log.d(TAG, "Tile data set for tile ID: $tileId") @@ -308,7 +305,7 @@ abstract class TileExtensions : TileService() { tileId = tileId, added = true, serverId = 0, - iconId = null, + iconName = null, entityId = "", label = "", subtitle = null, @@ -324,26 +321,17 @@ abstract class TileExtensions : TileService() { updateActiveTileServices(highestInUse, applicationContext) } - private fun getTileIcon(tileIconId: Int?, entity: Entity<*>?, context: Context): Bitmap? { + private fun getTileIcon(tileIconName: String?, entity: Entity<*>?, context: Context): Bitmap? { // Create an icon pack and load all drawables. - if (tileIconId != null) { - if (iconPack == null) { - val loader = IconPackLoader(context) - iconPack = createMaterialDesignIconPack(loader) - iconPack!!.loadDrawables(loader.drawableLoader) - } - - val iconDrawable = iconPack?.icons?.get(tileIconId)?.drawable - if (iconDrawable != null) { - return DrawableCompat.wrap(iconDrawable).toBitmap() - } + if (!tileIconName.isNullOrBlank()) { + val icon = CommunityMaterial.getIconByMdiName(tileIconName) ?: return null + val iconDrawable = IconicsDrawable(context, icon) + return iconDrawable.toBitmap() } else { entity?.getIcon(context)?.let { - return DrawableCompat.wrap( - IconicsDrawable(context, it).apply { - sizeDp = 48 - } - ).toBitmap() + return IconicsDrawable(context, it).apply { + sizeDp = 48 + }.toBitmap() } } @@ -352,7 +340,6 @@ abstract class TileExtensions : TileService() { companion object { private const val TAG = "TileExtensions" - private var iconPack: IconPack? = null private val toggleDomains = listOf( "automation", "cover", "fan", "humidifier", "input_boolean", "light", "media_player", "remote", "siren", "switch" diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesFragment.kt index ea07153d2..4c4a94a81 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesFragment.kt @@ -8,20 +8,23 @@ import android.view.LayoutInflater import android.view.Menu import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.accompanist.themeadapter.material.MdcTheme -import com.maltaisn.icondialog.IconDialog -import com.maltaisn.icondialog.IconDialogSettings -import com.maltaisn.icondialog.pack.IconPack +import com.mikepenz.iconics.typeface.IIcon import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.settings.qs.views.ManageTilesView +import io.homeassistant.companion.android.util.icondialog.IconDialog import io.homeassistant.companion.android.common.R as commonR @AndroidEntryPoint -class ManageTilesFragment : Fragment(), IconDialog.Callback { +class ManageTilesFragment : Fragment() { companion object { private const val TAG = "TileFragment" @@ -53,17 +56,24 @@ class ManageTilesFragment : Fragment(), IconDialog.Callback { savedInstanceState: Bundle? ): View { return ComposeView(requireContext()).apply { - val settings = IconDialogSettings { - searchVisibility = IconDialog.SearchVisibility.ALWAYS - } - val iconDialog = IconDialog.newInstance(settings) - setContent { MdcTheme { + var showingDialog by remember { mutableStateOf(false) } + + if (showingDialog) { + IconDialog( + onSelect = { + onIconDialogIconsSelected(it) + showingDialog = false + }, + onDismissRequest = { showingDialog = false } + ) + } + ManageTilesView( viewModel = viewModel, - onShowIconDialog = { tag -> - iconDialog.show(childFragmentManager, tag) + onShowIconDialog = { + showingDialog = true } ) } @@ -76,14 +86,8 @@ class ManageTilesFragment : Fragment(), IconDialog.Callback { activity?.title = getString(commonR.string.tiles) } - override val iconDialogIconPack: IconPack - get() = viewModel.iconPack - - override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List) { - Log.d(TAG, "Selected icon: ${icons.firstOrNull()}") - val selectedIcon = icons.firstOrNull() - if (selectedIcon != null) { - viewModel.selectIcon(selectedIcon) - } + private fun onIconDialogIconsSelected(selectedIcon: IIcon) { + Log.d(TAG, "Selected icon: ${selectedIcon.name}") + viewModel.selectIcon(selectedIcon) } } diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesViewModel.kt b/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesViewModel.kt index e989aae33..26696969a 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesViewModel.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/qs/ManageTilesViewModel.kt @@ -4,24 +4,19 @@ import android.annotation.SuppressLint import android.app.Application import android.app.StatusBarManager import android.content.ComponentName +import android.graphics.drawable.Icon import android.os.Build import android.util.Log -import androidx.appcompat.content.res.AppCompatResources import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.getSystemService -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.graphics.drawable.toBitmapOrNull import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.maltaisn.icondialog.data.Icon -import com.maltaisn.icondialog.pack.IconPack -import com.maltaisn.icondialog.pack.IconPackLoader -import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import dagger.hilt.android.lifecycle.HiltViewModel import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.domain @@ -72,6 +67,8 @@ import io.homeassistant.companion.android.qs.Tile6Service import io.homeassistant.companion.android.qs.Tile7Service import io.homeassistant.companion.android.qs.Tile8Service import io.homeassistant.companion.android.qs.Tile9Service +import io.homeassistant.companion.android.util.icondialog.getIconByMdiName +import io.homeassistant.companion.android.util.icondialog.mdiName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -139,8 +136,6 @@ class ManageTilesViewModel @Inject constructor( ) } - lateinit var iconPack: IconPack - private val app = application val slots = loadTileSlots(application.resources) @@ -154,9 +149,7 @@ class ManageTilesViewModel @Inject constructor( private set var selectedServerId by mutableStateOf(ServerManager.SERVER_ID_ACTIVE) private set - var selectedIconId by mutableStateOf(null) - private set - var selectedIconDrawable by mutableStateOf(AppCompatResources.getDrawable(application, commonR.drawable.ic_stat_ic_notification)) + var selectedIconId by mutableStateOf(null) private set var selectedEntityId by mutableStateOf("") var tileLabel by mutableStateOf("") @@ -165,6 +158,8 @@ class ManageTilesViewModel @Inject constructor( private set var selectedShouldVibrate by mutableStateOf(false) var tileAuthRequired by mutableStateOf(false) + + var selectedIcon: IIcon? = null private var selectedTileId = 0 private var selectedTileAdded = false @@ -202,16 +197,6 @@ class ManageTilesViewModel @Inject constructor( selectTile(slots.indexOf(selectedTile)) } } - - viewModelScope.launch(Dispatchers.IO) { - val loader = IconPackLoader(getApplication()) - iconPack = createMaterialDesignIconPack(loader) - iconPack.loadDrawables(loader.drawableLoader) - withContext(Dispatchers.Main) { - // The icon pack might not have been initialized when the tile data was loaded - selectTile(slots.indexOf(selectedTile)) - } - } } fun selectTile(index: Int) { @@ -259,21 +244,9 @@ class ManageTilesViewModel @Inject constructor( if (selectedIconId == null) selectIcon(null) // trigger drawable update } - fun selectIcon(icon: Icon?) { - selectedIconId = icon?.id - selectedIconDrawable = if (icon != null) { - icon.drawable?.let { DrawableCompat.wrap(it) } - } else { - sortedEntities.firstOrNull { it.entityId == selectedEntityId }?.let { - it.getIcon(app)?.let { iIcon -> - DrawableCompat.wrap( - IconicsDrawable(app, iIcon).apply { - sizeDp = 20 - } - ) - } - } - } + fun selectIcon(icon: IIcon?) { + selectedIconId = icon?.mdiName + selectedIcon = icon ?: sortedEntities.firstOrNull { it.entityId == selectedEntityId }?.getIcon(app) } private fun updateExistingTileFields(currentTile: TileEntity) { @@ -283,13 +256,7 @@ class ManageTilesViewModel @Inject constructor( selectedShouldVibrate = currentTile.shouldVibrate tileAuthRequired = currentTile.authRequired selectIcon( - currentTile.iconId?.let { - if (::iconPack.isInitialized) { - iconPack.getIcon(it) - } else { - null - } - } + currentTile.iconName?.let { CommunityMaterial.getIconByMdiName(it) } ) } @@ -300,7 +267,7 @@ class ManageTilesViewModel @Inject constructor( tileId = selectedTile.id, serverId = selectedServerId, added = selectedTileAdded, - iconId = selectedIconId, + iconName = selectedIconId, entityId = selectedEntityId, label = tileLabel, subtitle = tileSubtitle, @@ -315,11 +282,10 @@ class ManageTilesViewModel @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !selectedTileAdded) { val statusBarManager = app.getSystemService() val service = idToTileService[selectedTile.id] ?: Tile1Service::class.java - val icon = selectedIconDrawable?.let { - it.toBitmapOrNull(it.intrinsicWidth, it.intrinsicHeight)?.let { bitmap -> - android.graphics.drawable.Icon.createWithBitmap(bitmap) - } - } ?: android.graphics.drawable.Icon.createWithResource(app, commonR.drawable.ic_stat_ic_notification) + val icon = selectedIcon?.let { + val bitmap = IconicsDrawable(getApplication(), it).toBitmap() + Icon.createWithBitmap(bitmap) + } ?: Icon.createWithResource(app, commonR.drawable.ic_stat_ic_notification) statusBarManager?.requestAddTileService( ComponentName(app, service), diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/qs/views/ManageTilesView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/qs/views/ManageTilesView.kt index 1999323a0..c18742a11 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/qs/views/ManageTilesView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/qs/views/ManageTilesView.kt @@ -1,7 +1,6 @@ package io.homeassistant.companion.android.settings.qs.views import android.os.Build -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -31,13 +30,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.graphics.drawable.toBitmap import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.settings.qs.ManageTilesViewModel import io.homeassistant.companion.android.util.compose.ServerDropdownButton @@ -160,12 +157,9 @@ fun ManageTilesView( OutlinedButton( onClick = { onShowIconDialog(viewModel.selectedTile.id) } ) { - val iconBitmap = remember(viewModel.selectedIconDrawable) { - viewModel.selectedIconDrawable?.toBitmap()?.asImageBitmap() - } - iconBitmap?.let { - Image( - iconBitmap, + viewModel.selectedIcon?.let { icon -> + com.mikepenz.iconics.compose.Image( + icon, contentDescription = stringResource(id = R.string.tile_icon), colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent)), modifier = Modifier.size(20.dp) diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsSettingsFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsSettingsFragment.kt index 3d97a4b7d..55068fc0b 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsSettingsFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsSettingsFragment.kt @@ -1,7 +1,6 @@ package io.homeassistant.companion.android.settings.shortcuts import android.content.Intent -import android.graphics.PorterDuff import android.net.Uri import android.os.Build import android.os.Bundle @@ -11,28 +10,24 @@ import android.view.Menu import android.view.View import android.view.ViewGroup import androidx.annotation.RequiresApi +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView -import androidx.core.graphics.drawable.DrawableCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import com.google.accompanist.themeadapter.material.MdcTheme -import com.maltaisn.icondialog.IconDialog -import com.maltaisn.icondialog.IconDialogSettings -import com.maltaisn.icondialog.pack.IconPack -import com.maltaisn.icondialog.pack.IconPackLoader -import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack +import com.mikepenz.iconics.typeface.IIcon import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.settings.shortcuts.views.ManageShortcutsView -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import io.homeassistant.companion.android.util.icondialog.IconDialog import io.homeassistant.companion.android.common.R as commonR @RequiresApi(Build.VERSION_CODES.N_MR1) @AndroidEntryPoint -class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback { +class ManageShortcutsSettingsFragment : Fragment() { companion object { const val MAX_SHORTCUTS = 5 @@ -41,19 +36,10 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback { } val viewModel: ManageShortcutsViewModel by viewModels() - private lateinit var iconPack: IconPack override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - - lifecycleScope.launch { - withContext(Dispatchers.IO) { - val loader = IconPackLoader(requireContext()) - iconPack = createMaterialDesignIconPack(loader) - iconPack.loadDrawables(loader.drawableLoader) - } - } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -71,14 +57,20 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback { savedInstanceState: Bundle? ): View { return ComposeView(requireContext()).apply { - val settings = IconDialogSettings { - searchVisibility = IconDialog.SearchVisibility.ALWAYS - } - val iconDialog = IconDialog.newInstance(settings) - setContent { MdcTheme { - ManageShortcutsView(viewModel = viewModel, iconDialog = iconDialog, childFragment = childFragmentManager) + var showingTag by remember { mutableStateOf(null) } + showingTag?.let { tag -> + IconDialog( + onSelect = { + onIconDialogIconsSelected(tag, it) + showingTag = null + }, + onDismissRequest = { showingTag = null } + ) + } + + ManageShortcutsView(viewModel = viewModel, showIconDialog = { showingTag = it }) } } } @@ -92,44 +84,17 @@ class ManageShortcutsSettingsFragment : Fragment(), IconDialog.Callback { activity?.title = getString(commonR.string.shortcuts) } - override val iconDialogIconPack: IconPack - get() = iconPack + private fun onIconDialogIconsSelected(tag: String, selectedIcon: IIcon) { + Log.d(TAG, "Selected icon: $selectedIcon") - override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List) { - Log.d(TAG, "Selected icon: ${icons.firstOrNull()}") - val selectedIcon = icons.firstOrNull() - if (selectedIcon != null) { - val iconDrawable = selectedIcon.drawable - if (iconDrawable != null) { - val icon = DrawableCompat.wrap(iconDrawable) - icon.setColorFilter(resources.getColor(commonR.color.colorAccent), PorterDuff.Mode.SRC_IN) - when (dialog.tag) { - "shortcut_1" -> { - viewModel.shortcuts[0].selectedIcon.value = selectedIcon.id - viewModel.shortcuts[0].drawable.value = icon - } - "shortcut_2" -> { - viewModel.shortcuts[1].selectedIcon.value = selectedIcon.id - viewModel.shortcuts[1].drawable.value = icon - } - "shortcut_3" -> { - viewModel.shortcuts[2].selectedIcon.value = selectedIcon.id - viewModel.shortcuts[2].drawable.value = icon - } - "shortcut_4" -> { - viewModel.shortcuts[3].selectedIcon.value = selectedIcon.id - viewModel.shortcuts[3].drawable.value = icon - } - "shortcut_5" -> { - viewModel.shortcuts[4].selectedIcon.value = selectedIcon.id - viewModel.shortcuts[4].drawable.value = icon - } - else -> { - viewModel.shortcuts[5].selectedIcon.value = selectedIcon.id - viewModel.shortcuts[5].drawable.value = icon - } - } - } + val index = when (tag) { + "shortcut_1" -> 0 + "shortcut_2" -> 1 + "shortcut_3" -> 2 + "shortcut_4" -> 3 + "shortcut_5" -> 4 + else -> 5 } + viewModel.shortcuts[index].selectedIcon.value = selectedIcon } } diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsViewModel.kt b/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsViewModel.kt index 39319ddd9..857e2ac2e 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsViewModel.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/ManageShortcutsViewModel.kt @@ -4,35 +4,41 @@ import android.app.Application import android.content.Intent import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager -import android.graphics.Bitmap import android.graphics.PorterDuff -import android.graphics.drawable.Drawable +import android.graphics.PorterDuffColorFilter import android.graphics.drawable.Icon import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi -import androidx.appcompat.content.res.AppCompatResources import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import androidx.core.graphics.drawable.DrawableCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import com.maltaisn.icondialog.pack.IconPack -import com.maltaisn.icondialog.pack.IconPackLoader -import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.IconicsSize +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.padding +import com.mikepenz.iconics.utils.size import dagger.hilt.android.lifecycle.HiltViewModel import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.servers.ServerManager +import io.homeassistant.companion.android.database.IconDialogCompat +import io.homeassistant.companion.android.util.icondialog.getIconByMdiName +import io.homeassistant.companion.android.util.icondialog.mdiName import io.homeassistant.companion.android.webview.WebViewActivity import io.homeassistant.companion.android.widgets.assist.AssistShortcutActivity +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @RequiresApi(Build.VERSION_CODES.N_MR1) @@ -44,14 +50,13 @@ class ManageShortcutsViewModel @Inject constructor( val app = application private val TAG = "ShortcutViewModel" - private lateinit var iconPack: IconPack private var shortcutManager = application.applicationContext.getSystemService()!! val canPinShortcuts = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && shortcutManager.isRequestPinShortcutSupported var pinnedShortcuts = shortcutManager.pinnedShortcuts .filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) } .toMutableList() private set - var dynamicShortcuts: MutableList = shortcutManager.dynamicShortcuts + var dynamicShortcuts = mutableListOf() private set var servers by mutableStateOf(serverManager.defaultServers) @@ -61,15 +66,16 @@ class ManageShortcutsViewModel @Inject constructor( private val currentServerId = serverManager.getServer()?.id ?: 0 + private val iconIdToName: Map by lazy { IconDialogCompat(app.assets).loadAllIcons() } + data class Shortcut( var id: MutableState, var serverId: MutableState, - var selectedIcon: MutableState, + var selectedIcon: MutableState, var label: MutableState, var desc: MutableState, var path: MutableState, var type: MutableState, - var drawable: MutableState, var delete: MutableState ) @@ -90,6 +96,7 @@ class ManageShortcutsViewModel @Inject constructor( } } } + updateDynamicShortcuts() Log.d(TAG, "We have ${dynamicShortcuts.size} dynamic shortcuts") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -102,12 +109,11 @@ class ManageShortcutsViewModel @Inject constructor( Shortcut( mutableStateOf(""), mutableStateOf(currentServerId), - mutableStateOf(0), + mutableStateOf(null), mutableStateOf(""), mutableStateOf(""), mutableStateOf(""), mutableStateOf("lovelace"), - mutableStateOf(AppCompatResources.getDrawable(application, R.drawable.ic_stat_ic_notification_blue)), mutableStateOf(false) ) ) @@ -119,26 +125,31 @@ class ManageShortcutsViewModel @Inject constructor( } } - fun createShortcut(shortcutId: String, serverId: Int, shortcutLabel: String, shortcutDesc: String, shortcutPath: String, bitmap: Bitmap? = null, iconId: Int) { + fun createShortcut(shortcutId: String, serverId: Int, shortcutLabel: String, shortcutDesc: String, shortcutPath: String, icon: IIcon?) { Log.d(TAG, "Attempt to add shortcut $shortcutId") val intent = Intent( - WebViewActivity.newInstance(getApplication(), shortcutPath, serverId).addFlags( + WebViewActivity.newInstance(app, shortcutPath, serverId).addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ) ) intent.action = shortcutPath intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - intent.putExtra("iconId", iconId) + icon?.let { intent.putExtra("iconName", icon.mdiName) } - val shortcut = ShortcutInfo.Builder(getApplication(), shortcutId) + val shortcut = ShortcutInfo.Builder(app, shortcutId) .setShortLabel(shortcutLabel) .setLongLabel(shortcutDesc) .setIcon( - if (bitmap != null) { + if (icon != null) { + val bitmap = IconicsDrawable(app, icon).apply { + size = IconicsSize.dp(48) + padding = IconicsSize.dp(2) + colorFilter = PorterDuffColorFilter(ContextCompat.getColor(app, R.color.colorAccent), PorterDuff.Mode.SRC_IN) + }.toBitmap() Icon.createWithBitmap(bitmap) } else { - Icon.createWithResource(getApplication(), R.drawable.ic_stat_ic_notification_blue) + Icon.createWithResource(app, R.drawable.ic_stat_ic_notification_blue) } ) .setIntent(intent) @@ -146,7 +157,7 @@ class ManageShortcutsViewModel @Inject constructor( if (shortcutId.startsWith("shortcut")) { shortcutManager.addDynamicShortcuts(listOf(shortcut)) - dynamicShortcuts = shortcutManager.dynamicShortcuts + updateDynamicShortcuts() } else { var isNewPinned = true for (item in pinnedShortcuts) { @@ -154,7 +165,7 @@ class ManageShortcutsViewModel @Inject constructor( isNewPinned = false Log.d(TAG, "Updating pinned shortcut: $shortcutId") shortcutManager.updateShortcuts(listOf(shortcut)) - Toast.makeText(getApplication(), R.string.shortcut_updated, Toast.LENGTH_SHORT).show() + Toast.makeText(app, R.string.shortcut_updated, Toast.LENGTH_SHORT).show() } } @@ -169,64 +180,54 @@ class ManageShortcutsViewModel @Inject constructor( fun deleteShortcut(shortcutId: String) { shortcutManager.removeDynamicShortcuts(listOf(shortcutId)) - dynamicShortcuts = shortcutManager.dynamicShortcuts + updateDynamicShortcuts() } - fun setPinnedShortcutData(shortcutId: String) { + fun setPinnedShortcutData(shortcutId: String) = viewModelScope.launch { for (item in pinnedShortcuts) { if (item.id == shortcutId) { shortcuts.last().id.value = item.id - shortcuts.last().serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId - shortcuts.last().label.value = item.shortLabel.toString() - shortcuts.last().desc.value = item.longLabel.toString() - shortcuts.last().path.value = item.intent?.action.toString() - shortcuts.last().selectedIcon.value = item.intent?.extras?.getInt("iconId").toString().toIntOrNull() ?: 0 - if (shortcuts.last().selectedIcon.value != 0) { - shortcuts.last().drawable.value = getTileIcon(shortcuts.last().selectedIcon.value) - } - if (shortcuts.last().path.value.startsWith("entityId:")) { - shortcuts.last().type.value = "entityId" - } else { - shortcuts.last().type.value = "lovelace" - } + shortcuts.last().setData(item) } } } - fun setDynamicShortcutData(shortcutId: String, index: Int) { + private fun updateDynamicShortcuts() { + dynamicShortcuts = shortcutManager.dynamicShortcuts.sortedBy { it.id }.toMutableList() + } + + private fun setDynamicShortcutData(shortcutId: String, index: Int) = viewModelScope.launch { if (dynamicShortcuts.isNotEmpty()) { for (item in dynamicShortcuts) { if (item.id == shortcutId) { Log.d(TAG, "setting ${item.id} data") - shortcuts[index].serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId - shortcuts[index].label.value = item.shortLabel.toString() - shortcuts[index].desc.value = item.longLabel.toString() - shortcuts[index].path.value = item.intent?.action.toString() - shortcuts[index].selectedIcon.value = item.intent?.extras?.getInt("iconId").toString().toIntOrNull() ?: 0 - if (shortcuts[index].selectedIcon.value != 0) { - shortcuts[index].drawable.value = getTileIcon(shortcuts[index].selectedIcon.value) - } - if (shortcuts[index].path.value.startsWith("entityId:")) { - shortcuts[index].type.value = "entityId" - } else { - shortcuts[index].type.value = "lovelace" - } + shortcuts[index].setData(item) } } } } - private fun getTileIcon(tileIconId: Int): Drawable? { - val loader = IconPackLoader(getApplication()) - iconPack = createMaterialDesignIconPack(loader) - iconPack.loadDrawables(loader.drawableLoader) - val iconDrawable = iconPack.icons[tileIconId]?.drawable - if (iconDrawable != null) { - val icon = DrawableCompat.wrap(iconDrawable) - icon.setColorFilter(app.resources.getColor(R.color.colorAccent), PorterDuff.Mode.SRC_IN) - return icon + private suspend fun Shortcut.setData(item: ShortcutInfo) { + serverId.value = item.intent?.extras?.getInt("server", currentServerId) ?: currentServerId + label.value = item.shortLabel.toString() + desc.value = item.longLabel.toString() + path.value = item.intent?.action.toString() + selectedIcon.value = if (item.intent?.extras?.containsKey("iconName") == true) { + item.intent?.extras?.getString("iconName")?.let { CommunityMaterial.getIconByMdiName(it) } + } else if (item.intent?.extras?.containsKey("iconId") == true) { + withContext(Dispatchers.IO) { + item.intent?.extras?.getInt("iconId")?.takeIf { it != 0 }?.let { + CommunityMaterial.getIconByMdiName("mdi:${iconIdToName.getValue(it)}") + } + } + } else { + null + } + if (path.value.startsWith("entityId:")) { + type.value = "entityId" + } else { + type.value = "lovelace" } - return null } fun updatePinnedShortcuts() { diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/views/ManageShortcutsView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/views/ManageShortcutsView.kt index 39fa9011b..39007cdb8 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/views/ManageShortcutsView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/shortcuts/views/ManageShortcutsView.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.Button import androidx.compose.material.Divider @@ -25,16 +26,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.graphics.drawable.toBitmap -import androidx.fragment.app.FragmentManager -import com.maltaisn.icondialog.IconDialog +import com.mikepenz.iconics.compose.IconicsPainter import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsSettingsFragment import io.homeassistant.companion.android.settings.shortcuts.ManageShortcutsViewModel @@ -44,8 +42,7 @@ import io.homeassistant.companion.android.util.compose.ServerDropdownButton @Composable fun ManageShortcutsView( viewModel: ManageShortcutsViewModel, - iconDialog: IconDialog, - childFragment: FragmentManager + showIconDialog: (tag: String) -> Unit ) { LazyColumn(contentPadding = PaddingValues(16.dp)) { item { @@ -67,8 +64,7 @@ fun ManageShortcutsView( CreateShortcutView( i = i, viewModel = viewModel, - iconDialog = iconDialog, - childFragment = childFragment + showIconDialog = showIconDialog ) } } @@ -76,7 +72,11 @@ fun ManageShortcutsView( @RequiresApi(Build.VERSION_CODES.N_MR1) @Composable -private fun CreateShortcutView(i: Int, viewModel: ManageShortcutsViewModel, iconDialog: IconDialog, childFragment: FragmentManager) { +private fun CreateShortcutView( + i: Int, + viewModel: ManageShortcutsViewModel, + showIconDialog: (tag: String) -> Unit +) { val context = LocalContext.current var expandedEntity by remember { mutableStateOf(false) } var expandedPinnedShortcuts by remember { mutableStateOf(false) } @@ -155,17 +155,21 @@ private fun CreateShortcutView(i: Int, viewModel: ManageShortcutsViewModel, icon modifier = Modifier.padding(end = 10.dp) ) OutlinedButton(onClick = { - iconDialog.show(childFragment, shortcutId) + showIconDialog(shortcutId) }) { - val icon = viewModel.shortcuts[i].drawable.value?.let { DrawableCompat.wrap(it) } - icon?.toBitmap()?.asImageBitmap() - ?.let { - Image( - it, - contentDescription = stringResource(id = R.string.shortcut_icon), - colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent)) - ) - } + val icon = viewModel.shortcuts[i].selectedIcon.value + val painter = if (icon != null) { + remember(icon) { IconicsPainter(icon) } + } else { + painterResource(R.drawable.ic_stat_ic_notification_blue) + } + + Image( + painter = painter, + contentDescription = stringResource(id = R.string.shortcut_icon), + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(colorResource(R.color.colorAccent)) + ) } } @@ -269,7 +273,6 @@ private fun CreateShortcutView(i: Int, viewModel: ManageShortcutsViewModel, icon shortcut.label.value, shortcut.desc.value, shortcut.path.value, - shortcut.drawable.value?.toBitmap(), shortcut.selectedIcon.value ) }, diff --git a/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IIconKt.kt b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IIconKt.kt new file mode 100644 index 000000000..b1445d87b --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IIconKt.kt @@ -0,0 +1,24 @@ +package io.homeassistant.companion.android.util.icondialog + +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial + +const val MDI_PREFIX = "mdi:" + +/** + * Gets the MDI name of an Iconics icon. + * MDI format is used by Home Assistant (ie "mdi:account-alert"), + * compared to Iconic's [IIcon.name] format (ie "cmd_account_alert"). + */ +val IIcon.mdiName: String + get() = name.replace("${CommunityMaterial.mappingPrefix}_", MDI_PREFIX).replace('_', '-') + +fun CommunityMaterial.getIconByMdiName(mdiName: String): IIcon? { + val name = mdiName.replace(MDI_PREFIX, "${mappingPrefix}_").replace('-', '_') + return try { + getIcon(name) + } catch (e: IllegalArgumentException) { + // Icon doesn't exist (anymore) + null + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialog.kt b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialog.kt new file mode 100644 index 000000000..4cf18123f --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialog.kt @@ -0,0 +1,69 @@ +package io.homeassistant.companion.android.util.icondialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.google.accompanist.themeadapter.material.MdcTheme +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial + +@Composable +fun IconDialogContent( + iconFilter: IconFilter = DefaultIconFilter(), + onSelect: (IIcon) -> Unit +) { + var searchQuery by remember { mutableStateOf("") } + Column { + IconDialogSearch(value = searchQuery, onValueChange = { searchQuery = it }) + IconDialogGrid( + typeface = CommunityMaterial, + searchQuery = searchQuery, + iconFilter = iconFilter, + onClick = onSelect + ) + } +} + +@Composable +fun IconDialog( + iconFilter: IconFilter = DefaultIconFilter(), + onSelect: (IIcon) -> Unit, + onDismissRequest: () -> Unit +) { + Dialog(onDismissRequest = onDismissRequest) { + Surface( + modifier = Modifier + .width(480.dp) + .height(500.dp), + shape = MaterialTheme.shapes.medium + ) { + IconDialogContent(iconFilter = iconFilter, onSelect = onSelect) + } + } +} + +@Preview +@Composable +private fun IconDialogPreview() { + MdcTheme { + Surface( + modifier = Modifier + .width(480.dp) + .height(500.dp), + shape = MaterialTheme.shapes.medium + ) { + IconDialogContent(onSelect = {}) + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogFragment.kt b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogFragment.kt new file mode 100644 index 000000000..fbd8e7fc2 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogFragment.kt @@ -0,0 +1,57 @@ +package io.homeassistant.companion.android.util.icondialog + +import android.os.Bundle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.DialogFragment +import com.google.accompanist.themeadapter.material.MdcTheme +import com.mikepenz.iconics.typeface.IIcon +import kotlin.math.min + +class IconDialogFragment(callback: (IIcon) -> Unit) : DialogFragment() { + + companion object { + const val TAG = "IconDialogFragment" + } + + private val onSelect = callback + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).also { + it.clipToPadding = true + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (view as ComposeView).setContent { + MdcTheme { + IconDialogContent( + onSelect = onSelect + ) + } + } + } + + override fun onResume() { + super.onResume() + val params = dialog?.window?.attributes ?: return + params.width = min( + (resources.displayMetrics.widthPixels * 0.9).toInt(), + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 480f, resources.displayMetrics).toInt() + ) + params.height = min( + (resources.displayMetrics.heightPixels * 0.9).toInt(), + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 500f, resources.displayMetrics).toInt() + ) + dialog?.window?.attributes = params + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogGrid.kt b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogGrid.kt new file mode 100644 index 000000000..5d5feb530 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogGrid.kt @@ -0,0 +1,98 @@ +package io.homeassistant.companion.android.util.icondialog + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.themeadapter.material.MdcTheme +import com.mikepenz.iconics.compose.Image +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.ITypeface +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Display a grid of icons, letting the user select one. + * @param icons List of icons to display + * @param onClick Invoked when the user clicks on the given icon + */ +@Composable +fun IconDialogGrid( + icons: List, + tint: Color = MaterialTheme.colors.onSurface, + onClick: (IIcon) -> Unit +) { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 48.dp), + modifier = Modifier.fillMaxSize() + ) { + items(icons) { icon -> + IconButton(onClick = { onClick(icon) }) { + Image( + asset = icon, + colorFilter = ColorFilter.tint(tint), + // https://material.io/design/iconography/system-icons.html#color + alpha = 0.54f + ) + } + } + } +} + +/** + * Display a grid of icons, letting the user select one. + * @param typeface Icon typeface that includes all possible icons. + * @param searchQuery Search term used to filter icons from the [typeface]. + * @param iconFilter Adjust filtering logic for the search process. + * @param onClick Invoked when the user clicks on the given icon + */ +@Composable +fun IconDialogGrid( + typeface: ITypeface, + searchQuery: String, + iconFilter: IconFilter = DefaultIconFilter(), + tint: Color = MaterialTheme.colors.onSurface, + onClick: (IIcon) -> Unit +) { + var icons by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(typeface, searchQuery) { + icons = withContext(Dispatchers.IO) { iconFilter.queryIcons(typeface, searchQuery) } + } + + IconDialogGrid(icons = icons, tint = tint, onClick = onClick) +} + +@Preview +@Composable +private fun IconDialogGridPreview() { + MdcTheme { + Surface( + modifier = Modifier + .width(480.dp) + .height(500.dp), + shape = MaterialTheme.shapes.medium + ) { + IconDialogGrid( + icons = CommunityMaterial.icons.map { name -> CommunityMaterial.getIcon(name) }, + onClick = {} + ) + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogSearch.kt b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogSearch.kt new file mode 100644 index 000000000..3851987fa --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconDialogSearch.kt @@ -0,0 +1,60 @@ +package io.homeassistant.companion.android.util.icondialog + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.tooling.preview.Preview +import com.google.accompanist.themeadapter.material.MdcTheme +import io.homeassistant.companion.android.common.R + +@Composable +fun IconDialogSearch( + value: String, + onValueChange: (String) -> Unit +) { + val isEnglish by remember { mutableStateOf(Locale.current.language == "en") } + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + onValueChange = onValueChange, + singleLine = true, + label = { + Text(text = stringResource(if (isEnglish) R.string.search_icons else R.string.search_icons_in_english)) + }, + leadingIcon = { + Icon(Icons.Filled.Search, contentDescription = null) + }, + trailingIcon = if (value.isNotBlank()) { + { + IconButton(onClick = { onValueChange("") }) { + Icon(Icons.Filled.Clear, contentDescription = stringResource(R.string.clear_search)) + } + } + } else { + null + } + ) +} + +@Preview +@Composable +private fun IconDialogSearchPreview() { + MdcTheme { + Surface { + IconDialogSearch(value = "account", onValueChange = {}) + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconFilter.kt b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconFilter.kt new file mode 100644 index 000000000..12905a828 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/util/icondialog/IconFilter.kt @@ -0,0 +1,79 @@ +package io.homeassistant.companion.android.util.icondialog + +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.ITypeface +import java.text.Normalizer +import java.util.Locale + +/** + * Normalize [this] string, removing all diacritics, all unicode characters, hyphens, + * apostrophes and more. Resulting text has only lowercase latin letters and digits. + */ +private fun String.normalize(locale: Locale): String { + val normalized = this.lowercase(locale).trim().let { Normalizer.normalize(it, Normalizer.Form.NFKD) } + return normalized.filter { c -> c in 'a'..'z' || c in 'а'..'я' || c in '0'..'9' } +} + +/** + * Class used to filter the icons for search and sort them afterwards. + * Icon filter must be parcelable to be put in bundle. + */ +interface IconFilter { + + /** + * Get a list of all matching icons for a search [query], in no specific order. + */ + fun queryIcons(pack: ITypeface, query: String? = null): List +} + +class DefaultIconFilter( + /** + * Regex used to split the query into multiple search terms. + * Can also be null to not split the query. + */ + private val termSplitPattern: Regex? = """[;,\s]""".toRegex(), + /** + * Whether to normalize search query or not, using [String.normalize]. + */ + private val queryNormalized: Boolean = true +) : IconFilter { + + /** + * Get a list of all matching icons for a search [query]. + * Base implementation only returns the complete list of icons in the pack, + * sorted by ID. Subclasses take care of actual searching and must always ensure + * that the returned list is sorted by ID. + */ + override fun queryIcons(pack: ITypeface, query: String?): List { + val icons = pack.icons + + if (query == null || query.isBlank()) { + // No search query, return all icons. + return icons.map { key -> pack.getIcon(key) } + } + + // Split query into terms. + val trimmedQuery = query.trim() + val terms = (termSplitPattern?.let { trimmedQuery.split(it) } ?: listOf(trimmedQuery)) + .map { + if (queryNormalized) { + it.normalize(Locale.ROOT) + } else { + it.lowercase(Locale.ROOT) + } + }.filter { it.isNotBlank() } + + // Remove all icons that don't match any of the search terms. + return icons + .filter { icon -> matchesSearch(icon, terms) } + .map { key -> pack.getIcon(key) } + } + + /** + * Check if an [icon] name matches any of the search [terms]. + */ + private fun matchesSearch(icon: String, terms: List): Boolean { + val name = if (queryNormalized) icon.normalize(Locale.ROOT) else icon + return terms.any { it in name } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt index c38ff1e26..6f408a3cd 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidget.kt @@ -21,9 +21,11 @@ import androidx.core.graphics.toColorInt import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.material.color.DynamicColors -import com.maltaisn.icondialog.pack.IconPack -import com.maltaisn.icondialog.pack.IconPackLoader -import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.IconicsSize +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.padding +import com.mikepenz.iconics.utils.size import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.data.servers.ServerManager @@ -31,6 +33,7 @@ import io.homeassistant.companion.android.database.widget.ButtonWidgetDao import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.util.getAttribute +import io.homeassistant.companion.android.util.icondialog.getIconByMdiName import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -56,7 +59,7 @@ class ButtonWidget : AppWidgetProvider() { internal const val EXTRA_SERVICE = "EXTRA_SERVICE" internal const val EXTRA_SERVICE_DATA = "EXTRA_SERVICE_DATA" internal const val EXTRA_LABEL = "EXTRA_LABEL" - internal const val EXTRA_ICON = "EXTRA_ICON" + internal const val EXTRA_ICON_NAME = "EXTRA_ICON_NAME" internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE" internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR" internal const val EXTRA_REQUIRE_AUTHENTICATION = "EXTRA_REQUIRE_AUTHENTICATION" @@ -71,8 +74,6 @@ class ButtonWidget : AppWidgetProvider() { @Inject lateinit var buttonWidgetDao: ButtonWidgetDao - private var iconPack: IconPack? = null - private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) override fun onUpdate( @@ -176,12 +177,6 @@ class ButtonWidget : AppWidgetProvider() { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } - // Create an icon pack and load all drawables. - if (iconPack == null) { - val loader = IconPackLoader(context) - iconPack = createMaterialDesignIconPack(loader) - iconPack!!.loadDrawables(loader.drawableLoader) - } val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_button_wrapper_dynamiccolor else R.layout.widget_button_wrapper_default).apply { // Theming @@ -196,39 +191,41 @@ class ButtonWidget : AppWidgetProvider() { setLabelVisibility(this, widget) // Content - val iconId = widget?.iconId ?: 988171 // Lightning bolt + val iconData = widget?.iconName?.let { CommunityMaterial.getIconByMdiName(it) } + ?: CommunityMaterial.Icon2.cmd_flash // Lightning bolt - val iconDrawable = iconPack?.icons?.get(iconId)?.drawable - if (iconDrawable != null) { - val icon = DrawableCompat.wrap(iconDrawable) - if (widget?.backgroundType == WidgetBackgroundType.TRANSPARENT) { - setInt(R.id.widgetImageButton, "setColorFilter", textColor) - } - - // Determine reasonable dimensions for drawing vector icon as a bitmap - val aspectRatio = iconDrawable.intrinsicWidth / iconDrawable.intrinsicHeight.toDouble() - val awo = if (widget != null) AppWidgetManager.getInstance(context).getAppWidgetOptions(widget.id) else null - val maxWidth = ( - awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, DEFAULT_MAX_ICON_SIZE) - ?: DEFAULT_MAX_ICON_SIZE - ).coerceAtLeast(16) - val maxHeight = ( - awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, DEFAULT_MAX_ICON_SIZE) - ?: DEFAULT_MAX_ICON_SIZE - ).coerceAtLeast(16) - val width: Int - val height: Int - if (maxWidth > maxHeight) { - width = maxWidth - height = (maxWidth * (1 / aspectRatio)).toInt() - } else { - width = (maxHeight * aspectRatio).toInt() - height = maxHeight - } - - // Render the icon into the Button's ImageView - setImageViewBitmap(R.id.widgetImageButton, icon.toBitmap(width, height)) + val iconDrawable = IconicsDrawable(context, iconData).apply { + padding = IconicsSize.dp(2) + size = IconicsSize.dp(24) } + val icon = DrawableCompat.wrap(iconDrawable) + if (widget?.backgroundType == WidgetBackgroundType.TRANSPARENT) { + setInt(R.id.widgetImageButton, "setColorFilter", textColor) + } + + // Determine reasonable dimensions for drawing vector icon as a bitmap + val aspectRatio = iconDrawable.intrinsicWidth / iconDrawable.intrinsicHeight.toDouble() + val awo = if (widget != null) AppWidgetManager.getInstance(context).getAppWidgetOptions(widget.id) else null + val maxWidth = ( + awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, DEFAULT_MAX_ICON_SIZE) + ?: DEFAULT_MAX_ICON_SIZE + ).coerceAtLeast(16) + val maxHeight = ( + awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, DEFAULT_MAX_ICON_SIZE) + ?: DEFAULT_MAX_ICON_SIZE + ).coerceAtLeast(16) + val width: Int + val height: Int + if (maxWidth > maxHeight) { + width = maxWidth + height = (maxWidth * (1 / aspectRatio)).toInt() + } else { + width = (maxHeight * aspectRatio).toInt() + height = maxHeight + } + + // Render the icon into the Button's ImageView + setImageViewBitmap(R.id.widgetImageButton, icon.toBitmap(width, height)) setOnClickPendingIntent( R.id.widgetImageButtonLayout, @@ -364,7 +361,7 @@ class ButtonWidget : AppWidgetProvider() { val serviceData: String? = extras.getString(EXTRA_SERVICE_DATA) val label: String? = extras.getString(EXTRA_LABEL) val requireAuthentication: Boolean = extras.getBoolean(EXTRA_REQUIRE_AUTHENTICATION) - val icon: Int = extras.getInt(EXTRA_ICON) + val icon: String = extras.getString(EXTRA_ICON_NAME) ?: "mdi:flash" val backgroundType: WidgetBackgroundType = extras.getSerializable(EXTRA_BACKGROUND_TYPE) as WidgetBackgroundType val textColor: String? = extras.getString(EXTRA_TEXT_COLOR) diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt index da1ca4ecf..1ac061fa9 100644 --- a/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/button/ButtonWidgetConfigureActivity.kt @@ -5,6 +5,8 @@ import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Intent +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.os.Build import android.os.Bundle import android.text.Editable @@ -21,21 +23,18 @@ import android.widget.Spinner import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.toColorInt +import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.material.color.DynamicColors -import com.maltaisn.icondialog.IconDialog -import com.maltaisn.icondialog.IconDialogSettings -import com.maltaisn.icondialog.data.Icon -import com.maltaisn.icondialog.pack.IconPack -import com.maltaisn.icondialog.pack.IconPackLoader -import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import dagger.hilt.android.AndroidEntryPoint +import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.Service import io.homeassistant.companion.android.database.widget.ButtonWidgetDao @@ -43,6 +42,9 @@ import io.homeassistant.companion.android.database.widget.WidgetBackgroundType import io.homeassistant.companion.android.databinding.WidgetButtonConfigureBinding import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel import io.homeassistant.companion.android.util.getHexForColor +import io.homeassistant.companion.android.util.icondialog.IconDialogFragment +import io.homeassistant.companion.android.util.icondialog.getIconByMdiName +import io.homeassistant.companion.android.util.icondialog.mdiName import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity import io.homeassistant.companion.android.widgets.common.ServiceFieldBinder import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter @@ -52,10 +54,9 @@ import javax.inject.Inject import io.homeassistant.companion.android.common.R as commonR @AndroidEntryPoint -class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog.Callback { +class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity() { companion object { private const val TAG: String = "ButtonWidgetConfigAct" - private const val ICON_DIALOG_TAG = "icon-dialog" private const val PIN_WIDGET_CALLBACK = "io.homeassistant.companion.android.widgets.button.ButtonWidgetConfigureActivity.PIN_WIDGET_CALLBACK" } @@ -63,8 +64,6 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog. lateinit var buttonWidgetDao: ButtonWidgetDao override val dao get() = buttonWidgetDao - private lateinit var iconPack: IconPack - private var services = mutableMapOf>() private var entities = mutableMapOf>>() private var dynamicFields = ArrayList() @@ -258,9 +257,6 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog. setupServerSelect(buttonWidget?.serverId) - // Create an icon pack loader with application context. - val loader = IconPackLoader(this) - serviceAdapter = SingleItemArrayAdapter(this) { if (it != null) getServiceString(it) else "" } @@ -349,16 +345,20 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog. // Do this off the main thread, takes a second or two... runOnUiThread { // Create an icon pack and load all drawables. - iconPack = createMaterialDesignIconPack(loader) - iconPack.loadDrawables(loader.drawableLoader) - val settings = IconDialogSettings { - searchVisibility = IconDialog.SearchVisibility.ALWAYS - } - val iconDialog = IconDialog.newInstance(settings) - val iconId = buttonWidget?.iconId ?: 62017 - onIconDialogIconsSelected(iconDialog, listOf(iconPack.icons[iconId]!!)) + val iconName = buttonWidget?.iconName ?: "mdi:flash" + val icon = CommunityMaterial.getIconByMdiName(iconName) ?: CommunityMaterial.Icon2.cmd_flash + onIconDialogIconsSelected(icon) binding.widgetConfigIconSelector.setOnClickListener { - iconDialog.show(supportFragmentManager, ICON_DIALOG_TAG) + var alertDialog: DialogFragment? = null + + alertDialog = IconDialogFragment( + callback = { + onIconDialogIconsSelected(it) + alertDialog?.dismiss() + } + ) + + alertDialog.show(supportFragmentManager, IconDialogFragment.TAG) } } } @@ -400,23 +400,12 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog. } } - override val iconDialogIconPack: IconPack? - get() = iconPack + private fun onIconDialogIconsSelected(selectedIcon: IIcon) { + binding.widgetConfigIconSelector.tag = selectedIcon.mdiName + val iconDrawable = IconicsDrawable(this, selectedIcon) + iconDrawable.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(this, commonR.color.colorIcon), PorterDuff.Mode.SRC_IN) - override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List) { - Log.d(TAG, "Selected icon: ${icons.firstOrNull()}") - val selectedIcon = icons.firstOrNull() - if (selectedIcon != null) { - binding.widgetConfigIconSelector.tag = selectedIcon.id - val iconDrawable = selectedIcon.drawable - if (iconDrawable != null) { - val icon = DrawableCompat.wrap(iconDrawable) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - DrawableCompat.setTint(icon, resources.getColor(commonR.color.colorIcon, theme)) - } - binding.widgetConfigIconSelector.setImageBitmap(icon.toBitmap()) - } - } + binding.widgetConfigIconSelector.setImageBitmap(iconDrawable.toBitmap()) } private fun onAddWidget() { @@ -459,8 +448,8 @@ class ButtonWidgetConfigureActivity : BaseWidgetConfigureActivity(), IconDialog. binding.label.text.toString() ) intent.putExtra( - ButtonWidget.EXTRA_ICON, - binding.widgetConfigIconSelector.tag as Int + ButtonWidget.EXTRA_ICON_NAME, + binding.widgetConfigIconSelector.tag as String ) // Analyze and send service data diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 993eb7c64..44d1c2695 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -45,6 +45,9 @@ android { java { srcDirs("../app/src/main/java") } + assets { + srcDirs("../app/src/main/assets") + } res { srcDirs("../app/src/main/res") } @@ -169,8 +172,6 @@ dependencies { implementation("com.github.Dimezis:BlurView:version-1.6.6") implementation("org.altbeacon:android-beacon-library:2.19.5") - implementation("com.maltaisn:icondialog:3.3.0") - implementation("com.maltaisn:iconpack-community-material:5.3.45") implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.22") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") diff --git a/common/schemas/io.homeassistant.companion.android.database.AppDatabase/41.json b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/41.json new file mode 100644 index 000000000..bba8b8496 --- /dev/null +++ b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/41.json @@ -0,0 +1,994 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "5ccc437900bf203ab7db2e782ffbd7f9", + "entities": [ + { + "tableName": "sensor_attributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))", + "fields": [ + { + "fieldPath": "sensorId", + "columnName": "sensor_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "value_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "authentication_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sensors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT DEFAULT NULL, `last_sent_icon` TEXT DEFAULT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`, `server_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registered", + "columnName": "registered", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSentState", + "columnName": "last_sent_state", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastSentIcon", + "columnName": "last_sent_icon", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "stateType", + "columnName": "state_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceClass", + "columnName": "device_class", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unitOfMeasurement", + "columnName": "unit_of_measurement", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "stateClass", + "columnName": "state_class", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "entityCategory", + "columnName": "entity_category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coreRegistration", + "columnName": "core_registration", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "appRegistration", + "columnName": "app_registration", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "server_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sensor_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))", + "fields": [ + { + "fieldPath": "sensorId", + "columnName": "sensor_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "value_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entries", + "columnName": "entries", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "button_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "service", + "columnName": "service", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceData", + "columnName": "service_data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "requireAuthentication", + "columnName": "require_authentication", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "camera_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "media_player_controls_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `show_skip` INTEGER NOT NULL, `show_seek` INTEGER NOT NULL, `show_volume` INTEGER NOT NULL, `show_source` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showSkip", + "columnName": "show_skip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSeek", + "columnName": "show_seek", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showVolume", + "columnName": "show_volume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSource", + "columnName": "show_source", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "static_widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributeIds", + "columnName": "attribute_ids", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "stateSeparator", + "columnName": "state_separator", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributeSeparator", + "columnName": "attribute_separator", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "last_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "template_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "template", + "columnName": "template", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "notNull": true, + "defaultValue": "12.0" + }, + { + "fieldPath": "lastUpdate", + "columnName": "last_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL, `server_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "qs_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tileId", + "columnName": "tile_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldVibrate", + "columnName": "should_vibrate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "authRequired", + "columnName": "auth_required", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `friendly_name` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "friendlyName", + "columnName": "friendly_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "entity_state_complications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `show_title` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showTitle", + "columnName": "show_title", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_name` TEXT NOT NULL, `name_override` TEXT, `_version` TEXT, `list_order` INTEGER NOT NULL, `device_name` TEXT, `external_url` TEXT NOT NULL, `internal_url` TEXT, `cloud_url` TEXT, `webhook_id` TEXT, `secret` TEXT, `cloudhook_url` TEXT, `use_cloud` INTEGER NOT NULL, `internal_ssids` TEXT NOT NULL, `prioritize_internal` INTEGER NOT NULL, `access_token` TEXT, `refresh_token` TEXT, `token_expiration` INTEGER, `token_type` TEXT, `install_id` TEXT, `user_id` TEXT, `user_name` TEXT, `user_is_owner` INTEGER, `user_is_admin` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_name", + "columnName": "_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameOverride", + "columnName": "name_override", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "_version", + "columnName": "_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceName", + "columnName": "device_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.externalUrl", + "columnName": "external_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.internalUrl", + "columnName": "internal_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.cloudUrl", + "columnName": "cloud_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.webhookId", + "columnName": "webhook_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.cloudhookUrl", + "columnName": "cloudhook_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.useCloud", + "columnName": "use_cloud", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connection.internalSsids", + "columnName": "internal_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.prioritizeInternal", + "columnName": "prioritize_internal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "session.accessToken", + "columnName": "access_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.refreshToken", + "columnName": "refresh_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.tokenExpiration", + "columnName": "token_expiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "session.tokenType", + "columnName": "token_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.installId", + "columnName": "install_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.name", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.isOwner", + "columnName": "user_is_owner", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "user.isAdmin", + "columnName": "user_is_admin", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocket_setting` TEXT NOT NULL, `sensor_update_frequency` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "websocketSetting", + "columnName": "websocket_setting", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensorUpdateFrequency", + "columnName": "sensor_update_frequency", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ccc437900bf203ab7db2e782ffbd7f9')" + ] + } +} \ No newline at end of file diff --git a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt index 7a557675d..a6d489730 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt @@ -1,9 +1,12 @@ package io.homeassistant.companion.android.database +import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager import android.content.ContentValues import android.content.Context +import android.content.res.AssetManager +import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.os.Build import android.os.Handler @@ -14,6 +17,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.edit import androidx.core.content.getSystemService +import androidx.core.database.getStringOrNull import androidx.room.AutoMigration import androidx.room.Database import androidx.room.OnConflictStrategy @@ -65,7 +69,6 @@ import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity import io.homeassistant.companion.android.database.widget.WidgetBackgroundTypeConverter import kotlinx.coroutines.runBlocking import java.util.UUID -import kotlin.collections.ArrayList import io.homeassistant.companion.android.common.R as commonR @Database( @@ -87,7 +90,7 @@ import io.homeassistant.companion.android.common.R as commonR Server::class, Setting::class ], - version = 40, + version = 41, autoMigrations = [ AutoMigration(from = 24, to = 25), AutoMigration(from = 25, to = 26), @@ -175,12 +178,25 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_20_21, MIGRATION_21_22, MIGRATION_22_23, - MIGRATION_23_24 + MIGRATION_23_24, + Migration40to41(context.assets) ) .fallbackToDestructiveMigration() .build() } + private fun Cursor.map(transform: (Cursor) -> T): List { + return if (moveToFirst()) { + val results = mutableListOf() + do { + results.add(transform(this)) + } while (moveToNext()) + results + } else { + emptyList() + } + } + private val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( @@ -210,22 +226,22 @@ abstract class AppDatabase : RoomDatabase() { } private val MIGRATION_5_6 = object : Migration(5, 6) { + @SuppressLint("Range") override fun migrate(database: SupportSQLiteDatabase) { try { - val contentValues: ArrayList = ArrayList() val widgets = database.query("SELECT * FROM `static_widget`") widgets.use { if (widgets.count > 0) { - while (widgets.moveToNext()) { - val cv = ContentValues() - cv.put("id", widgets.getInt(widgets.getColumnIndex("id"))) - cv.put("entity_id", widgets.getString(widgets.getColumnIndex("entity_id"))) - cv.put("attribute_ids", widgets.getString(widgets.getColumnIndex("attribute_id"))) - cv.put("label", widgets.getString(widgets.getColumnIndex("label"))) - cv.put("text_size", widgets.getFloat(widgets.getColumnIndex("text_size"))) - cv.put("state_separator", widgets.getString(widgets.getColumnIndex("separator"))) - cv.put("attribute_separator", " ") - contentValues.add(cv) + val contentValues = widgets.map { widgets -> + ContentValues().apply { + put("id", widgets.getInt(widgets.getColumnIndex("id"))) + put("entity_id", widgets.getString(widgets.getColumnIndex("entity_id"))) + put("attribute_ids", widgets.getString(widgets.getColumnIndex("attribute_id"))) + put("label", widgets.getString(widgets.getColumnIndex("label"))) + put("text_size", widgets.getFloat(widgets.getColumnIndex("text_size"))) + put("state_separator", widgets.getString(widgets.getColumnIndex("separator"))) + put("attribute_separator", " ") + } } database.execSQL("DROP TABLE IF EXISTS `static_widget`") database.execSQL("CREATE TABLE IF NOT EXISTS `static_widget` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` FLOAT NOT NULL DEFAULT '30', `state_separator` TEXT NOT NULL DEFAULT '', `attribute_separator` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))") @@ -245,44 +261,38 @@ abstract class AppDatabase : RoomDatabase() { } private val MIGRATION_6_7 = object : Migration(6, 7) { + @SuppressLint("Range") override fun migrate(database: SupportSQLiteDatabase) { - val cursor = database.query("SELECT * FROM sensors") - val sensors = mutableListOf() - var migrationSuccessful = false var migrationFailed = false - try { - if (cursor.moveToFirst()) { - while (cursor.moveToNext()) { - sensors.add( - ContentValues().also { - it.put("id", cursor.getString(cursor.getColumnIndex("unique_id"))) - it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) - it.put( - "registered", - cursor.getInt(cursor.getColumnIndex("registered")) - ) - it.put("state", "") - it.put("state_type", "") - it.put("type", "") - it.put("icon", "") - it.put("name", "") - it.put("device_class", "") - } - ) + val sensors = try { + database.query("SELECT * FROM sensors").use { cursor -> + cursor.map { + ContentValues().also { + it.put("id", cursor.getString(cursor.getColumnIndex("unique_id"))) + it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) + it.put( + "registered", + cursor.getInt(cursor.getColumnIndex("registered")) + ) + it.put("state", "") + it.put("state_type", "") + it.put("type", "") + it.put("icon", "") + it.put("name", "") + it.put("device_class", "") + } } - migrationSuccessful = true } - cursor.close() } catch (e: Exception) { migrationFailed = true Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) + null } database.execSQL("DROP TABLE IF EXISTS `sensors`") database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))") - if (migrationSuccessful) { - sensors.forEach { - database.insert("sensors", OnConflictStrategy.REPLACE, it) - } + + sensors?.forEach { + database.insert("sensors", OnConflictStrategy.REPLACE, it) } if (migrationFailed) { notifyMigrationFailed() @@ -305,44 +315,38 @@ abstract class AppDatabase : RoomDatabase() { } private val MIGRATION_9_10 = object : Migration(9, 10) { + @SuppressLint("Range") override fun migrate(database: SupportSQLiteDatabase) { - val cursor = database.query("SELECT * FROM sensors") - val sensors = mutableListOf() - var migrationSuccessful = false var migrationFailed = false - try { - if (cursor.moveToFirst()) { - while (cursor.moveToNext()) { - sensors.add( - ContentValues().also { - it.put("id", cursor.getString(cursor.getColumnIndex("id"))) - it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) - it.put( - "registered", - cursor.getInt(cursor.getColumnIndex("registered")) - ) - it.put("state", "") - it.put("last_sent_state", "") - it.put("state_type", "") - it.put("type", "") - it.put("icon", "") - it.put("name", "") - } - ) + val sensors = try { + database.query("SELECT * FROM sensors").use { cursor -> + cursor.map { + ContentValues().also { + it.put("id", cursor.getString(cursor.getColumnIndex("id"))) + it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) + it.put( + "registered", + cursor.getInt(cursor.getColumnIndex("registered")) + ) + it.put("state", "") + it.put("last_sent_state", "") + it.put("state_type", "") + it.put("type", "") + it.put("icon", "") + it.put("name", "") + } } - migrationSuccessful = true } - cursor.close() } catch (e: Exception) { migrationFailed = true Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) + null } database.execSQL("DROP TABLE IF EXISTS `sensors`") database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))") - if (migrationSuccessful) { - sensors.forEach { - database.insert("sensors", OnConflictStrategy.REPLACE, it) - } + + sensors?.forEach { + database.insert("sensors", OnConflictStrategy.REPLACE, it) } if (migrationFailed) { notifyMigrationFailed() @@ -388,6 +392,7 @@ abstract class AppDatabase : RoomDatabase() { } private val MIGRATION_16_17 = object : Migration(16, 17) { + @SuppressLint("Range") override fun migrate(database: SupportSQLiteDatabase) { val cursor = database.query("SELECT * FROM sensor_settings") val sensorSettings = mutableListOf() @@ -472,19 +477,17 @@ abstract class AppDatabase : RoomDatabase() { } ) } - migrationSuccessful = true } - cursor.close() } catch (e: Exception) { migrationFailed = true Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) + null } database.execSQL("DROP TABLE IF EXISTS `sensor_settings`") database.execSQL("CREATE TABLE IF NOT EXISTS `sensor_settings` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL DEFAULT 'string', `entries` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT '1', PRIMARY KEY(`sensor_id`, `name`))") - if (migrationSuccessful) { - sensorSettings.forEach { - database.insert("sensor_settings", OnConflictStrategy.REPLACE, it) - } + + sensorSettings?.forEach { + database.insert("sensor_settings", OnConflictStrategy.REPLACE, it) } if (migrationFailed) { notifyMigrationFailed() @@ -811,6 +814,86 @@ abstract class AppDatabase : RoomDatabase() { } } + class Migration40to41(assets: AssetManager) : Migration(40, 41) { + private val iconIdToName: Map by lazy { IconDialogCompat(assets).loadAllIcons() } + + private fun Cursor.getIconName(columnIndex: Int): String { + val iconId = getInt(columnIndex) + return "mdi:${iconIdToName.getValue(iconId)}" + } + + @SuppressLint("Range") + override fun migrate(database: SupportSQLiteDatabase) { + var migrationFailed = false + val widgets = try { + database.query("SELECT * FROM `button_widgets`").use { cursor -> + cursor.map { + ContentValues().apply { + put("id", cursor.getString(cursor.getColumnIndex("id"))) + put("server_id", cursor.getInt(cursor.getColumnIndex("server_id"))) + put("domain", cursor.getString(cursor.getColumnIndex("domain"))) + put("service", cursor.getString(cursor.getColumnIndex("service"))) + put("service_data", cursor.getString(cursor.getColumnIndex("service_data"))) + put("label", cursor.getStringOrNull(cursor.getColumnIndex("label"))) + put("background_type", cursor.getString(cursor.getColumnIndex("background_type"))) + put("text_color", cursor.getStringOrNull(cursor.getColumnIndex("text_color"))) + put("require_authentication", cursor.getInt(cursor.getColumnIndex("require_authentication"))) + + put("icon_name", cursor.getIconName(cursor.getColumnIndex("icon_id"))) + } + } + } + } catch (e: Exception) { + migrationFailed = true + Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) + null + } + database.execSQL("DROP TABLE IF EXISTS `button_widgets`") + database.execSQL("CREATE TABLE IF NOT EXISTS `button_widgets` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))") + widgets?.forEach { + database.insert("button_widgets", OnConflictStrategy.REPLACE, it) + } + Log.d(TAG, "Migrated ${widgets?.size ?: "no"} button widgets to MDI icon names") + + val tiles = try { + database.query("SELECT * FROM `qs_tiles`").use { cursor -> + cursor.map { + ContentValues().apply { + put("id", cursor.getString(cursor.getColumnIndex("id"))) + put("tile_id", cursor.getString(cursor.getColumnIndex("tile_id"))) + put("added", cursor.getInt(cursor.getColumnIndex("added"))) + put("server_id", cursor.getInt(cursor.getColumnIndex("server_id"))) + put("entity_id", cursor.getString(cursor.getColumnIndex("entity_id"))) + put("label", cursor.getString(cursor.getColumnIndex("label"))) + put("subtitle", cursor.getStringOrNull(cursor.getColumnIndex("subtitle"))) + put("should_vibrate", cursor.getInt(cursor.getColumnIndex("should_vibrate"))) + put("auth_required", cursor.getInt(cursor.getColumnIndex("auth_required"))) + + val oldIconColumn = cursor.getColumnIndex("icon_id") + if (!cursor.isNull(oldIconColumn)) { + put("icon_name", cursor.getIconName(oldIconColumn)) + } + } + } + } + } catch (e: Exception) { + migrationFailed = true + Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e) + null + } + database.execSQL("DROP TABLE IF EXISTS `qs_tiles`") + database.execSQL("CREATE TABLE IF NOT EXISTS `qs_tiles` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)") + tiles?.forEach { + database.insert("qs_tiles", OnConflictStrategy.REPLACE, it) + } + Log.d(TAG, "Migrated ${tiles?.size ?: "no"} QS tiles to MDI icon names") + + if (migrationFailed) { + notifyMigrationFailed() + } + } + } + private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = appContext.getSystemService()!! diff --git a/common/src/main/java/io/homeassistant/companion/android/database/IconDialogCompat.kt b/common/src/main/java/io/homeassistant/companion/android/database/IconDialogCompat.kt new file mode 100644 index 000000000..87e06fd1a --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/database/IconDialogCompat.kt @@ -0,0 +1,64 @@ +package io.homeassistant.companion.android.database + +import android.content.res.AssetManager +import android.util.JsonReader +import android.util.NoSuchPropertyException +import androidx.annotation.WorkerThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * Translation layer for IDs used by the old icondialog package to material icon names. + */ +class IconDialogCompat @Inject constructor( + private val assets: AssetManager +) { + /** + * Loads map of icon IDs to regular icon names. + */ + @WorkerThread + fun loadAllIcons(): Map { + val inputStream = assets.open("mdi_id_map.json") + return JsonReader(inputStream.reader()).use { reader -> + val result = mutableMapOf() + reader.beginObject() + while (reader.hasNext()) { + val iconName = reader.nextName() + val iconId = reader.nextInt() + result[iconId] = iconName + } + reader.endObject() + result + } + } + + /** + * Loads map of icon IDs to regular icon names in a background thread. + */ + suspend fun loadAllIconsAsync() = coroutineScope { + async(Dispatchers.IO) { loadAllIcons() } + } + + suspend fun streamingIconLookup(iconId: Int): String { + val iconName = withContext(Dispatchers.IO) { + val inputStream = assets.open("mdi_id_map.json") + JsonReader(inputStream.reader()).use { reader -> + reader.beginObject() + while (reader.hasNext()) { + val iconName = reader.nextName() + val id = reader.nextInt() + if (iconId == id) { + return@use iconName + } + } + reader.endObject() + + throw NoSuchPropertyException("ID $iconId is not valid") + } + } + return iconName + } +} diff --git a/common/src/main/java/io/homeassistant/companion/android/database/qs/TileEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/qs/TileEntity.kt index ce320d2dd..a09b87c76 100755 --- a/common/src/main/java/io/homeassistant/companion/android/database/qs/TileEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/qs/TileEntity.kt @@ -14,8 +14,9 @@ data class TileEntity( val added: Boolean, @ColumnInfo(name = "server_id", defaultValue = "0") val serverId: Int, - @ColumnInfo(name = "icon_id") - val iconId: Int?, + /** Icon name, such as "mdi:account-alert" */ + @ColumnInfo(name = "icon_name") + val iconName: String?, @ColumnInfo(name = "entity_id") val entityId: String, @ColumnInfo(name = "label") diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt index c6914890f..3e7d33fb1 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/ButtonWidgetEntity.kt @@ -10,8 +10,8 @@ data class ButtonWidgetEntity( override val id: Int, @ColumnInfo(name = "server_id", defaultValue = "0") override val serverId: Int, - @ColumnInfo(name = "icon_id") - val iconId: Int, + @ColumnInfo(name = "icon_name") + val iconName: String, @ColumnInfo(name = "domain") val domain: String, @ColumnInfo(name = "service") diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 2d0f8b558..b0f9d8dd2 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -120,8 +120,9 @@ Choose entity Choose server Clear Favorites + Clear search Close - Color temperature: %1$s + Color temperature: %1$d Collapse Invalid entity Entity state @@ -486,6 +487,8 @@ Scene Scenes Scripts + Search icons + Search icons (in English) Search notifications Search Results Search sensors